mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2d63e69ea | ||
|
|
9e344f56e0 |
@@ -9,7 +9,6 @@ using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Service.AppCenter;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using System.Diagnostics;
|
||||
using Windows.Storage;
|
||||
@@ -29,13 +28,13 @@ public partial class App : Application
|
||||
/// </summary>
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="appCenter">App Center</param>
|
||||
public App(ILogger<App> logger, AppCenter appCenter)
|
||||
public App(ILogger<App> logger)
|
||||
{
|
||||
// load app resource
|
||||
InitializeComponent();
|
||||
this.logger = logger;
|
||||
|
||||
_ = new ExceptionRecorder(this, logger, appCenter);
|
||||
_ = new ExceptionRecorder(this, logger);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -61,10 +60,6 @@ public partial class App : Application
|
||||
.ImplictAs<IMetadataInitializer>()?
|
||||
.InitializeInternalAsync()
|
||||
.SafeForget(logger);
|
||||
|
||||
Ioc.Default
|
||||
.GetRequiredService<AppCenter>()
|
||||
.Initialize();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -18,5 +18,5 @@ internal interface ISupportAsyncInitialization
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>初始化任务</returns>
|
||||
ValueTask<bool> InitializeAsync(CancellationToken token = default);
|
||||
ValueTask<bool> InitializeAsync();
|
||||
}
|
||||
@@ -15,9 +15,6 @@ namespace Snap.Hutao.Core;
|
||||
/// </summary>
|
||||
internal static class CoreEnvironment
|
||||
{
|
||||
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
|
||||
private const string MachineGuidValue = "MachineGuid";
|
||||
|
||||
// 计算过程:https://gist.github.com/Lightczx/373c5940b36e24b25362728b52dec4fd
|
||||
|
||||
/// <summary>
|
||||
@@ -56,9 +53,9 @@ internal static class CoreEnvironment
|
||||
public static readonly string HoyolabDeviceId;
|
||||
|
||||
/// <summary>
|
||||
/// AppCenter 设备Id
|
||||
/// 胡桃设备Id
|
||||
/// </summary>
|
||||
public static readonly string AppCenterDeviceId;
|
||||
public static readonly string HutaoDeviceId;
|
||||
|
||||
/// <summary>
|
||||
/// 默认的Json序列化选项
|
||||
@@ -71,6 +68,9 @@ internal static class CoreEnvironment
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
private const string CryptographyKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\";
|
||||
private const string MachineGuidValue = "MachineGuid";
|
||||
|
||||
static CoreEnvironment()
|
||||
{
|
||||
Version = Package.Current.Id.Version.ToVersion();
|
||||
@@ -78,7 +78,7 @@ internal static class CoreEnvironment
|
||||
|
||||
// simply assign a random guid
|
||||
HoyolabDeviceId = Guid.NewGuid().ToString();
|
||||
AppCenterDeviceId = GetUniqueUserID();
|
||||
HutaoDeviceId = GetUniqueUserID();
|
||||
}
|
||||
|
||||
private static string GetUniqueUserID()
|
||||
|
||||
@@ -26,7 +26,6 @@ internal class DbCurrent<TEntity, TMessage>
|
||||
/// </summary>
|
||||
/// <param name="dbSet">数据集</param>
|
||||
/// <param name="messenger">消息器</param>
|
||||
///
|
||||
public DbCurrent(DbSet<TEntity> dbSet, IMessenger messenger)
|
||||
{
|
||||
this.dbSet = dbSet;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
@@ -91,4 +92,4 @@ public static class DbSetExtension
|
||||
dbSet.Update(entity);
|
||||
return dbSet.Context().SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
|
||||
namespace Snap.Hutao.Core.Database;
|
||||
|
||||
/// <summary>
|
||||
/// 设置帮助类
|
||||
/// </summary>
|
||||
public static class SettingEntryHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或添加一个对应的设置
|
||||
/// </summary>
|
||||
/// <param name="dbSet">设置集</param>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>设置</returns>
|
||||
public static SettingEntry SingleOrAdd(this DbSet<SettingEntry> dbSet, string key, string value)
|
||||
{
|
||||
SettingEntry? entry = dbSet.SingleOrDefault(entry => key == entry.Key);
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
entry = new(key, value);
|
||||
dbSet.Add(entry);
|
||||
dbSet.Context().SaveChanges();
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或添加一个对应的设置
|
||||
/// </summary>
|
||||
/// <param name="dbSet">设置集</param>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="valueFactory">值工厂</param>
|
||||
/// <returns>设置</returns>
|
||||
public static SettingEntry SingleOrAdd(this DbSet<SettingEntry> dbSet, string key, Func<string> valueFactory)
|
||||
{
|
||||
SettingEntry? entry = dbSet.SingleOrDefault(entry => key == entry.Key);
|
||||
|
||||
if (entry == null)
|
||||
{
|
||||
entry = new(key, valueFactory());
|
||||
dbSet.Add(entry);
|
||||
dbSet.Context().SaveChanges();
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Boolean 值
|
||||
/// </summary>
|
||||
/// <param name="entry">设置</param>
|
||||
/// <returns>值</returns>
|
||||
public static bool GetBoolean(this SettingEntry entry)
|
||||
{
|
||||
return bool.Parse(entry.Value!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 Boolean 值
|
||||
/// </summary>
|
||||
/// <param name="entry">设置</param>
|
||||
/// <param name="value">值</param>
|
||||
public static void SetBoolean(this SettingEntry entry, bool value)
|
||||
{
|
||||
entry.Value = value.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Int32 值
|
||||
/// </summary>
|
||||
/// <param name="entry">设置</param>
|
||||
/// <returns>值</returns>
|
||||
public static int GetInt32(this SettingEntry entry)
|
||||
{
|
||||
return int.Parse(entry.Value!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 Int32 值
|
||||
/// </summary>
|
||||
/// <param name="entry">设置</param>
|
||||
/// <param name="value">值</param>
|
||||
public static void SetInt32(this SettingEntry entry, int value)
|
||||
{
|
||||
entry.Value = value.ToString();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Service.AppCenter;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
|
||||
namespace Snap.Hutao.Core.Exception;
|
||||
|
||||
@@ -13,26 +13,24 @@ namespace Snap.Hutao.Core.Exception;
|
||||
internal class ExceptionRecorder
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
private readonly AppCenter appCenter;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的异常记录器
|
||||
/// </summary>
|
||||
/// <param name="application">应用程序</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="appCenter">App Center</param>
|
||||
public ExceptionRecorder(Application application, ILogger logger, AppCenter appCenter)
|
||||
public ExceptionRecorder(Application application, ILogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.appCenter = appCenter;
|
||||
|
||||
application.UnhandledException += OnAppUnhandledException;
|
||||
application.DebugSettings.BindingFailed += OnXamlBindingFailed;
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD002")]
|
||||
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
appCenter.TrackCrash(e.Exception);
|
||||
Ioc.Default.GetRequiredService<HomaClient2>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
|
||||
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");
|
||||
|
||||
foreach (ILoggerProvider provider in Ioc.Default.GetRequiredService<IEnumerable<ILoggerProvider>>())
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Core.IO.Ini;
|
||||
internal static class IniSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步反序列化
|
||||
/// 反序列化
|
||||
/// </summary>
|
||||
/// <param name="fileStream">文件流</param>
|
||||
/// <returns>Ini 元素集合</returns>
|
||||
@@ -44,4 +44,20 @@ internal static class IniSerializer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 序列化
|
||||
/// </summary>
|
||||
/// <param name="fileStream">写入的流</param>
|
||||
/// <param name="elements">元素</param>
|
||||
public static void Serialize(FileStream fileStream, IEnumerable<IniElement> elements)
|
||||
{
|
||||
using (TextWriter writer = new StreamWriter(fileStream))
|
||||
{
|
||||
foreach (IniElement element in elements)
|
||||
{
|
||||
writer.WriteLine(element.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace Snap.Hutao.Core.LifeCycle;
|
||||
|
||||
@@ -20,6 +21,19 @@ internal static class Activation
|
||||
|
||||
private static readonly SemaphoreSlim ActivateSemaphore = new(1);
|
||||
|
||||
/// <summary>
|
||||
/// 获取是否提升了权限
|
||||
/// </summary>
|
||||
/// <returns>是否提升了权限</returns>
|
||||
public static bool GetElevated()
|
||||
{
|
||||
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
|
||||
{
|
||||
WindowsPrincipal principal = new(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 响应激活事件
|
||||
/// 激活事件一般不会在UI线程上触发
|
||||
@@ -70,6 +84,18 @@ internal static class Activation
|
||||
|
||||
case LaunchGame:
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
if (!MainWindow.IsPresent)
|
||||
{
|
||||
_ = Ioc.Default.GetRequiredService<LaunchGameWindow>();
|
||||
}
|
||||
else
|
||||
{
|
||||
await Ioc.Default
|
||||
.GetRequiredService<INavigationService>()
|
||||
.NavigateAsync<View.Page.LaunchGamePage>(INavigationAwaiter.Default, true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the task for a cancellation token, as well as the token registration. The registration is disposed when this instance is disposed.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">包装类型</typeparam>
|
||||
public sealed class CancellationTokenTaskCompletionSource : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The cancellation token registration, if any. This is <c>null</c> if the registration was not necessary.
|
||||
/// </summary>
|
||||
private readonly IDisposable? registration;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a task for the specified cancellation token, registering with the token if necessary.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token to observe.</param>
|
||||
public CancellationTokenTaskCompletionSource(CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
Task = Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource();
|
||||
registration = cancellationToken.Register(() => tcs.TrySetResult(), useSynchronizationContext: false);
|
||||
Task = tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the task for the source cancellation token.
|
||||
/// </summary>
|
||||
public Task Task { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the cancellation token registration, if any. Note that this may cause <see cref="Task"/> to never complete.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
registration?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -28,4 +28,4 @@ internal class ConcurrentCancellationTokenSource<TItem>
|
||||
|
||||
return waitingItems.GetOrAdd(item, new CancellationTokenSource()).Token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ namespace Snap.Hutao.Core;
|
||||
/// </summary>
|
||||
internal abstract class WebView2Helper
|
||||
{
|
||||
private static bool hasEverDetected = false;
|
||||
private static bool isSupported = false;
|
||||
private static bool hasEverDetected;
|
||||
private static bool isSupported;
|
||||
private static string version = "未检测到 WebView2 运行时";
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -18,16 +18,18 @@ namespace Snap.Hutao.Core.Windowing;
|
||||
/// 窗口管理器
|
||||
/// 主要包含了针对窗体的 P/Inoke 逻辑
|
||||
/// </summary>
|
||||
internal sealed class ExtendedWindow
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
internal sealed class ExtendedWindow<TWindow>
|
||||
where TWindow : Window, IExtendedWindowSource
|
||||
{
|
||||
private readonly HWND handle;
|
||||
private readonly AppWindow appWindow;
|
||||
|
||||
private readonly Window window;
|
||||
private readonly TWindow window;
|
||||
private readonly FrameworkElement titleBar;
|
||||
|
||||
private readonly ILogger<ExtendedWindow> logger;
|
||||
private readonly WindowSubclassManager subclassManager;
|
||||
private readonly ILogger<ExtendedWindow<TWindow>> logger;
|
||||
private readonly WindowSubclassManager<TWindow> subclassManager;
|
||||
|
||||
private readonly bool useLegacyDragBar;
|
||||
|
||||
@@ -36,11 +38,11 @@ internal sealed class ExtendedWindow
|
||||
/// </summary>
|
||||
/// <param name="window">窗口</param>
|
||||
/// <param name="titleBar">充当标题栏的元素</param>
|
||||
private ExtendedWindow(Window window, FrameworkElement titleBar)
|
||||
private ExtendedWindow(TWindow window, FrameworkElement titleBar)
|
||||
{
|
||||
this.window = window;
|
||||
this.titleBar = titleBar;
|
||||
logger = Ioc.Default.GetRequiredService<ILogger<ExtendedWindow>>();
|
||||
logger = Ioc.Default.GetRequiredService<ILogger<ExtendedWindow<TWindow>>>();
|
||||
|
||||
handle = (HWND)WindowNative.GetWindowHandle(window);
|
||||
|
||||
@@ -48,7 +50,7 @@ internal sealed class ExtendedWindow
|
||||
appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
|
||||
useLegacyDragBar = !AppWindowTitleBar.IsCustomizationSupported();
|
||||
subclassManager = new(handle, useLegacyDragBar);
|
||||
subclassManager = new(window, handle, useLegacyDragBar);
|
||||
|
||||
InitializeWindow();
|
||||
}
|
||||
@@ -57,11 +59,10 @@ internal sealed class ExtendedWindow
|
||||
/// 初始化
|
||||
/// </summary>
|
||||
/// <param name="window">窗口</param>
|
||||
/// <param name="titleBar">标题栏</param>
|
||||
/// <returns>实例</returns>
|
||||
public static ExtendedWindow Initialize(Window window, FrameworkElement titleBar)
|
||||
public static ExtendedWindow<TWindow> Initialize(TWindow window)
|
||||
{
|
||||
return new(window, titleBar);
|
||||
return new(window, window.TitleBar);
|
||||
}
|
||||
|
||||
private static void UpdateTitleButtonColor(AppWindowTitleBar appTitleBar)
|
||||
@@ -103,7 +104,8 @@ internal sealed class ExtendedWindow
|
||||
appWindow.Title = "胡桃";
|
||||
|
||||
ExtendsContentIntoTitleBar();
|
||||
Persistence.RecoverOrInit(appWindow);
|
||||
|
||||
Persistence.RecoverOrInit(appWindow, window.PersistSize, window.InitSize);
|
||||
|
||||
// Log basic window state here.
|
||||
(string pos, string size) = GetPostionAndSize(appWindow);
|
||||
@@ -115,14 +117,18 @@ internal sealed class ExtendedWindow
|
||||
logger.LogInformation(EventIds.BackdropState, "Apply {name} : {result}", nameof(SystemBackdrop), micaApplied ? "succeed" : "failed");
|
||||
|
||||
bool subClassApplied = subclassManager.TrySetWindowSubclass();
|
||||
logger.LogInformation(EventIds.SubClassing, "Apply {name} : {result}", nameof(WindowSubclassManager), subClassApplied ? "succeed" : "failed");
|
||||
logger.LogInformation(EventIds.SubClassing, "Apply {name} : {result}", nameof(WindowSubclassManager<TWindow>), subClassApplied ? "succeed" : "failed");
|
||||
|
||||
window.Closed += OnWindowClosed;
|
||||
}
|
||||
|
||||
private void OnWindowClosed(object sender, WindowEventArgs args)
|
||||
{
|
||||
Persistence.Save(appWindow);
|
||||
if (window.PersistSize)
|
||||
{
|
||||
Persistence.Save(appWindow);
|
||||
}
|
||||
|
||||
subclassManager?.Dispose();
|
||||
}
|
||||
|
||||
@@ -155,4 +161,4 @@ internal sealed class ExtendedWindow
|
||||
RectInt32 dragRect = new RectInt32(48, 0, (int)titleBar.ActualWidth, (int)titleBar.ActualHeight).Scale(scale);
|
||||
appTitleBar.SetDragRectangles(dragRect.Enumerate().ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao.Core.Windowing;
|
||||
|
||||
/// <summary>
|
||||
/// 为扩展窗体提供必要的选项
|
||||
/// </summary>
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
internal interface IExtendedWindowSource
|
||||
{
|
||||
/// <summary>
|
||||
/// 提供的标题栏
|
||||
/// </summary>
|
||||
FrameworkElement TitleBar { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否持久化尺寸
|
||||
/// </summary>
|
||||
bool PersistSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始大小
|
||||
/// </summary>
|
||||
SizeInt32 InitSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 处理最大最小信息
|
||||
/// </summary>
|
||||
/// <param name="pInfo">信息指针</param>
|
||||
/// <param name="scalingFactor">缩放比</param>
|
||||
unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor);
|
||||
}
|
||||
@@ -21,21 +21,26 @@ internal static class Persistence
|
||||
/// 设置窗体位置
|
||||
/// </summary>
|
||||
/// <param name="appWindow">应用窗体</param>
|
||||
public static void RecoverOrInit(AppWindow appWindow)
|
||||
/// <param name="persistSize">持久化尺寸</param>
|
||||
/// <param name="size">初始尺寸</param>
|
||||
public static void RecoverOrInit(AppWindow appWindow, bool persistSize, SizeInt32 size)
|
||||
{
|
||||
// Set first launch size.
|
||||
HWND hwnd = (HWND)Win32Interop.GetWindowFromWindowId(appWindow.Id);
|
||||
SizeInt32 size = TransformSizeForWindow(new(1200, 741), hwnd);
|
||||
RectInt32 rect = StructMarshal.RectInt32(size);
|
||||
SizeInt32 transformedSize = TransformSizeForWindow(size, hwnd);
|
||||
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
|
||||
|
||||
RectInt32 target = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
|
||||
if (target.Width * target.Height < 848 * 524)
|
||||
if (persistSize)
|
||||
{
|
||||
target = rect;
|
||||
RectInt32 persistedSize = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (ulong)(CompactRect)rect);
|
||||
if (persistedSize.Width * persistedSize.Height > 848 * 524)
|
||||
{
|
||||
rect = persistedSize;
|
||||
}
|
||||
}
|
||||
|
||||
TransformToCenterScreen(ref target);
|
||||
appWindow.MoveAndResize(target);
|
||||
TransformToCenterScreen(ref rect);
|
||||
appWindow.MoveAndResize(rect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -101,7 +101,7 @@ public class SystemBackdrop
|
||||
|
||||
private class DispatcherQueueHelper
|
||||
{
|
||||
private object? dispatcherQueueController = null;
|
||||
private object? dispatcherQueueController;
|
||||
|
||||
/// <summary>
|
||||
/// 确保系统调度队列控制器存在
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
@@ -11,14 +12,14 @@ namespace Snap.Hutao.Core.Windowing;
|
||||
/// <summary>
|
||||
/// 窗体子类管理器
|
||||
/// </summary>
|
||||
internal class WindowSubclassManager : IDisposable
|
||||
/// <typeparam name="TWindow">窗体类型</typeparam>
|
||||
internal class WindowSubclassManager<TWindow> : IDisposable
|
||||
where TWindow : Window, IExtendedWindowSource
|
||||
{
|
||||
private const int WindowSubclassId = 101;
|
||||
private const int DragBarSubclassId = 102;
|
||||
|
||||
private const int MinWidth = 848;
|
||||
private const int MinHeight = 524;
|
||||
|
||||
private readonly TWindow window;
|
||||
private readonly HWND hwnd;
|
||||
private readonly bool isLegacyDragBar;
|
||||
private HWND hwndDragBar;
|
||||
@@ -30,12 +31,13 @@ internal class WindowSubclassManager : IDisposable
|
||||
/// <summary>
|
||||
/// 构造一个新的窗体子类管理器
|
||||
/// </summary>
|
||||
/// <param name="window">窗体实例</param>
|
||||
/// <param name="hwnd">窗体句柄</param>
|
||||
/// <param name="isLegacyDragBar">是否为经典标题栏区域</param>
|
||||
public WindowSubclassManager(HWND hwnd, bool isLegacyDragBar)
|
||||
public WindowSubclassManager(TWindow window, HWND hwnd, bool isLegacyDragBar)
|
||||
{
|
||||
Must.NotNull(hwnd);
|
||||
this.hwnd = hwnd;
|
||||
this.window = window;
|
||||
this.hwnd = Must.NotNull(hwnd);
|
||||
this.isLegacyDragBar = isLegacyDragBar;
|
||||
}
|
||||
|
||||
@@ -85,9 +87,7 @@ internal class WindowSubclassManager : IDisposable
|
||||
case WM_GETMINMAXINFO:
|
||||
{
|
||||
double scalingFactor = Persistence.GetScaleForWindow(hwnd);
|
||||
MINMAXINFO* info = (MINMAXINFO*)lParam.Value;
|
||||
info->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, info->ptMinTrackSize.X);
|
||||
info->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, info->ptMinTrackSize.Y);
|
||||
window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Service.AppCenter;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
|
||||
namespace Snap.Hutao.Factory;
|
||||
|
||||
@@ -13,17 +13,14 @@ namespace Snap.Hutao.Factory;
|
||||
internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
|
||||
{
|
||||
private readonly ILogger<AsyncRelayCommandFactory> logger;
|
||||
private readonly AppCenter appCenter;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的异步命令工厂
|
||||
/// </summary>
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="appCenter">App Center</param>
|
||||
public AsyncRelayCommandFactory(ILogger<AsyncRelayCommandFactory> logger, AppCenter appCenter)
|
||||
public AsyncRelayCommandFactory(ILogger<AsyncRelayCommandFactory> logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.appCenter = appCenter;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -86,6 +83,7 @@ internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
|
||||
return command;
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD002")]
|
||||
private void ReportException(IAsyncRelayCommand command)
|
||||
{
|
||||
command.PropertyChanged += (sender, args) =>
|
||||
@@ -98,7 +96,7 @@ internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
|
||||
{
|
||||
Exception baseException = exception.GetBaseException();
|
||||
logger.LogError(EventIds.AsyncCommandException, baseException, "{name} Exception", nameof(AsyncRelayCommand));
|
||||
appCenter.TrackError(exception);
|
||||
Ioc.Default.GetRequiredService<HomaClient2>().UploadLogAsync(baseException).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,63 @@
|
||||
<Window
|
||||
x:Class="Snap.Hutao.LaunchGameWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Snap.Hutao"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
||||
xmlns:shv="using:Snap.Hutao.ViewModel"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
|
||||
<Grid
|
||||
Name="RootGrid"
|
||||
d:DataContext="{d:DesignInstance shv:LaunchGameViewModel}">
|
||||
|
||||
<mxi:Interaction.Behaviors>
|
||||
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
|
||||
</mxi:Interaction.Behaviors>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid
|
||||
x:Name="DragableGrid"
|
||||
Grid.Row="0"
|
||||
Height="32">
|
||||
<TextBlock
|
||||
Text="选择账号并启动"
|
||||
TextWrapping="NoWrap"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="12,0,0,0"/>
|
||||
</Grid>
|
||||
|
||||
<ListView
|
||||
Grid.Row="1"
|
||||
ItemsSource="{Binding GameAccounts}"
|
||||
SelectedItem="{Binding SelectedGameAccount,Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<StackPanel Margin="0,12">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
<TextBlock
|
||||
Opacity="0.8"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{Binding AttachUid,TargetNullValue=该账号尚未绑定 UID}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
|
||||
<Button
|
||||
Margin="16"
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
Content="启动游戏"
|
||||
Command="{Binding LaunchCommand}"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -1,20 +1,63 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Snap.Hutao.ViewModel;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏窗口
|
||||
/// </summary>
|
||||
public sealed partial class LaunchGameWindow : Window
|
||||
[Injection(InjectAs.Singleton)]
|
||||
public sealed partial class LaunchGameWindow : Window, IDisposable, IExtendedWindowSource
|
||||
{
|
||||
private const int MinWidth = 240;
|
||||
private const int MinHeight = 240;
|
||||
|
||||
private const int MaxWidth = 320;
|
||||
private const int MaxHeight = 320;
|
||||
|
||||
private readonly IServiceScope scope;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的启动游戏窗口
|
||||
/// </summary>
|
||||
public LaunchGameWindow()
|
||||
/// <param name="scopeFactory">范围工厂</param>
|
||||
public LaunchGameWindow(IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
InitializeComponent();
|
||||
ExtendedWindow<LaunchGameWindow>.Initialize(this);
|
||||
|
||||
scope = scopeFactory.CreateScope();
|
||||
RootGrid.DataContext = scope.ServiceProvider.GetRequiredService<LaunchGameViewModel>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FrameworkElement TitleBar { get => DragableGrid; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PersistSize { get => false; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SizeInt32 InitSize { get => new(320, 320); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
scope.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor)
|
||||
{
|
||||
pInfo->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, pInfo->ptMinTrackSize.X);
|
||||
pInfo->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, pInfo->ptMinTrackSize.Y);
|
||||
pInfo->ptMaxTrackSize.X = (int)Math.Min(MaxWidth * scalingFactor, pInfo->ptMaxTrackSize.X);
|
||||
pInfo->ptMaxTrackSize.Y = (int)Math.Min(MaxHeight * scalingFactor, pInfo->ptMaxTrackSize.Y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Windows.Graphics;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
@@ -11,14 +13,40 @@ namespace Snap.Hutao;
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton)]
|
||||
[SuppressMessage("", "CA1001")]
|
||||
public sealed partial class MainWindow : Window
|
||||
public sealed partial class MainWindow : Window, IExtendedWindowSource
|
||||
{
|
||||
private const int MinWidth = 848;
|
||||
private const int MinHeight = 524;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的主窗体
|
||||
/// </summary>
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
ExtendedWindow.Initialize(this, TitleBarView.DragArea);
|
||||
ExtendedWindow<MainWindow>.Initialize(this);
|
||||
IsPresent = true;
|
||||
Closed += (s, e) => IsPresent = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否打开
|
||||
/// </summary>
|
||||
public static bool IsPresent { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FrameworkElement TitleBar { get => TitleBarView.DragArea; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool PersistSize { get => true; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SizeInt32 InitSize { get => new(1200, 741); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe void ProcessMinMaxInfo(MINMAXINFO* pInfo, double scalingFactor)
|
||||
{
|
||||
pInfo->ptMinTrackSize.X = (int)Math.Max(MinWidth * scalingFactor, pInfo->ptMinTrackSize.X);
|
||||
pInfo->ptMinTrackSize.Y = (int)Math.Max(MinHeight * scalingFactor, pInfo->ptMinTrackSize.Y);
|
||||
}
|
||||
}
|
||||
215
src/Snap.Hutao/Snap.Hutao/Migrations/20221031104940_GameAccount.Designer.cs
generated
Normal file
215
src/Snap.Hutao/Snap.Hutao/Migrations/20221031104940_GameAccount.Designer.cs
generated
Normal file
@@ -0,0 +1,215 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Snap.Hutao.Context.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Snap.Hutao.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20221031104940_GameAccount")]
|
||||
partial class GameAccount
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ArchiveId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Current")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Time")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ArchiveId");
|
||||
|
||||
b.ToTable("achievements");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSelected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("achievement_archives");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Info")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Uid")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("avatar_infos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSelected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Uid")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("gacha_archives");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ArchiveId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("GachaType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ItemId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("QueryType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Time")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.HasIndex("ArchiveId");
|
||||
|
||||
b.ToTable("gacha_items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AttachUid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MihoyoSDK")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("game_accounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Cookie")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSelected")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
|
||||
.WithMany()
|
||||
.HasForeignKey("ArchiveId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Archive");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
|
||||
{
|
||||
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
|
||||
.WithMany()
|
||||
.HasForeignKey("ArchiveId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Archive");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Snap.Hutao.Migrations
|
||||
{
|
||||
public partial class GameAccount : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "game_accounts",
|
||||
columns: table => new
|
||||
{
|
||||
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
AttachUid = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
MihoyoSDK = table.Column<string>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_game_accounts", x => x.InnerId);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "game_accounts");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.9");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
|
||||
{
|
||||
@@ -131,6 +131,31 @@ namespace Snap.Hutao.Migrations
|
||||
b.ToTable("gacha_items");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("InnerId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AttachUid")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MihoyoSDK")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("InnerId");
|
||||
|
||||
b.ToTable("game_accounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
|
||||
@@ -8,12 +8,6 @@ namespace Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
/// </summary>
|
||||
public class Reliquary : EquipBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 副属性列表
|
||||
/// </summary>
|
||||
[Obsolete]
|
||||
public List<ReliquarySubProperty> SubProperties { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 初始词条
|
||||
/// </summary>
|
||||
|
||||
@@ -56,7 +56,7 @@ public class User : ObservableObject
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="EntityUser.Cookie"/>
|
||||
public Cookie Cookie
|
||||
public Cookie? Cookie
|
||||
{
|
||||
get => inner.Cookie;
|
||||
set
|
||||
@@ -71,7 +71,7 @@ public class User : ObservableObject
|
||||
/// </summary>
|
||||
public bool HasSToken
|
||||
{
|
||||
get => inner.Cookie.ContainsSToken();
|
||||
get => inner.Cookie!.ContainsSToken();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -84,17 +84,6 @@ public class User : ObservableObject
|
||||
/// </summary>
|
||||
public bool IsInitialized { get => isInitialized; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新SToken
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="cookie">cookie</param>
|
||||
internal void UpdateSToken(string uid, Cookie cookie)
|
||||
{
|
||||
Cookie.InsertSToken(uid, cookie);
|
||||
OnPropertyChanged(nameof(HasSToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从数据库恢复用户
|
||||
/// </summary>
|
||||
@@ -125,6 +114,17 @@ public class User : ObservableObject
|
||||
return successful ? user : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新SToken
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="cookie">cookie</param>
|
||||
internal void UpdateSToken(string uid, Cookie cookie)
|
||||
{
|
||||
Cookie!.InsertSToken(uid, cookie);
|
||||
OnPropertyChanged(nameof(HasSToken));
|
||||
}
|
||||
|
||||
private async Task<bool> InitializeCoreAsync(UserClient userClient, BindingClient userGameRoleClient, CancellationToken token = default)
|
||||
{
|
||||
if (isInitialized)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Model.Binding.LaunchGame;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
@@ -13,8 +11,11 @@ namespace Snap.Hutao.Model.Entity;
|
||||
/// 游戏内账号
|
||||
/// </summary>
|
||||
[Table("game_accounts")]
|
||||
public class GameAccount : ISelectable
|
||||
public class GameAccount : INotifyPropertyChanged
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 内部Id
|
||||
/// </summary>
|
||||
@@ -22,9 +23,6 @@ public class GameAccount : ISelectable
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public Guid InnerId { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsSelected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 对应的Uid
|
||||
/// </summary>
|
||||
@@ -41,7 +39,43 @@ public class GameAccount : ISelectable
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// MIHOYOSDK_ADL_PROD_CN_h3123967166
|
||||
/// [MIHOYOSDK_ADL_PROD_CN_h3123967166]
|
||||
/// see <see cref="Service.Game.GameAccountRegistryInterop.SdkKey"/>
|
||||
/// </summary>
|
||||
public string MihoyoSDK { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的游戏内账号
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <param name="sdk">sdk</param>
|
||||
/// <returns>游戏内账号</returns>
|
||||
public static GameAccount Create(string name, string sdk)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Name = name,
|
||||
MihoyoSDK = sdk,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新绑定的Uid
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
public void UpdateAttachUid(string? uid)
|
||||
{
|
||||
AttachUid = uid;
|
||||
PropertyChanged?.Invoke(this, new(nameof(AttachUid)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新名称
|
||||
/// </summary>
|
||||
/// <param name="name">新名称</param>
|
||||
public void UpdateName(string name)
|
||||
{
|
||||
Name = name;
|
||||
PropertyChanged?.Invoke(this, new(nameof(Name)));
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,36 @@ public class SettingEntry
|
||||
/// </summary>
|
||||
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 全屏
|
||||
/// </summary>
|
||||
public const string LaunchIsFullScreen = "Launch.IsFullScreen";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 无边框
|
||||
/// </summary>
|
||||
public const string LaunchIsBorderless = "Launch.IsBorderless";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 宽度
|
||||
/// </summary>
|
||||
public const string LaunchScreenWidth = "Launch.ScreenWidth";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 高度
|
||||
/// </summary>
|
||||
public const string LaunchScreenHeight = "Launch.ScreenHeight";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 解锁帧率
|
||||
/// </summary>
|
||||
public const string LaunchUnlockFps = "Launch.UnlockFps";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 目标帧率
|
||||
/// </summary>
|
||||
public const string LaunchTargetFps = "Launch.TargetFps";
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的设置入口
|
||||
/// </summary>
|
||||
|
||||
@@ -29,7 +29,7 @@ public class User : ISelectable
|
||||
/// <summary>
|
||||
/// 用户的Cookie
|
||||
/// </summary>
|
||||
public Cookie Cookie { get; set; } = default!;
|
||||
public Cookie? Cookie { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个新的用户
|
||||
|
||||
@@ -31,6 +31,6 @@ public class UIAF
|
||||
/// <returns>当前UIAF对象是否受支持</returns>
|
||||
public bool IsCurrentVersionSupported()
|
||||
{
|
||||
return SupportedVersion.Contains(Info.UIAFVersion ?? string.Empty);
|
||||
return SupportedVersion.Contains(Info?.UIAFVersion ?? string.Empty);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,9 @@ public static class AvatarIds
|
||||
public static readonly AvatarId Ayaka = 10000002;
|
||||
public static readonly AvatarId Qin = 10000003;
|
||||
|
||||
public static readonly AvatarId PlayerBoy = 10000005;
|
||||
public static readonly AvatarId Lisa = 10000006;
|
||||
public static readonly AvatarId PlayerGirl = 10000007;
|
||||
|
||||
public static readonly AvatarId Barbara = 10000014;
|
||||
public static readonly AvatarId Kaeya = 10000015;
|
||||
@@ -75,4 +77,16 @@ public static class AvatarIds
|
||||
public static readonly AvatarId Candace = 10000072;
|
||||
public static readonly AvatarId Nahida = 10000073;
|
||||
public static readonly AvatarId Layla = 10000074;
|
||||
public static readonly AvatarId Wanderer = 10000075;
|
||||
public static readonly AvatarId Faruzan = 10000076;
|
||||
|
||||
/// <summary>
|
||||
/// 检查该角色是否为主角
|
||||
/// </summary>
|
||||
/// <param name="avatarId">角色Id</param>
|
||||
/// <returns>角色是否为主角</returns>
|
||||
public static bool IsPlayer(AvatarId avatarId)
|
||||
{
|
||||
return avatarId == PlayerBoy || avatarId == PlayerGirl;
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,22 @@
|
||||
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
IgnorableNamespaces="uap desktop6 rescap">
|
||||
|
||||
<Identity
|
||||
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
|
||||
Publisher="CN=DGP Studio"
|
||||
Version="1.1.14.0" />
|
||||
Version="1.1.20.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>胡桃</DisplayName>
|
||||
<PublisherDisplayName>DGP Studio</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
|
||||
<desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
@@ -50,6 +53,7 @@
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
<rescap:Capability Name="runFullTrust"/>
|
||||
<rescap:Capability Name="unvirtualizedResources"/>
|
||||
</Capabilities>
|
||||
</Package>
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Service.AppCenter.Model;
|
||||
using Snap.Hutao.Service.AppCenter.Model.Log;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
[Injection(InjectAs.Singleton)]
|
||||
public sealed class AppCenter : IDisposable
|
||||
{
|
||||
private const string AppSecret = "de5bfc48-17fc-47ee-8e7e-dee7dc59d554";
|
||||
private const string API = "https://in.appcenter.ms/logs?api-version=1.0.0";
|
||||
|
||||
private readonly TaskCompletionSource uploadTaskCompletionSource = new();
|
||||
private readonly CancellationTokenSource uploadTaskCancllationTokenSource = new();
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly List<Log> queue;
|
||||
private readonly Device deviceInfo;
|
||||
private readonly JsonSerializerOptions options;
|
||||
|
||||
private Guid sessionID;
|
||||
|
||||
public AppCenter()
|
||||
{
|
||||
options = new(CoreEnvironment.JsonOptions);
|
||||
options.Converters.Add(new LogConverter());
|
||||
|
||||
httpClient = new() { DefaultRequestHeaders = { { "Install-ID", CoreEnvironment.AppCenterDeviceId }, { "App-Secret", AppSecret } } };
|
||||
queue = new List<Log>();
|
||||
deviceInfo = new Device();
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (!uploadTaskCancllationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
await UploadAsync().ConfigureAwait(false);
|
||||
await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
uploadTaskCompletionSource.TrySetResult();
|
||||
}).SafeForget();
|
||||
}
|
||||
|
||||
public async Task UploadAsync()
|
||||
{
|
||||
if (queue.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string? uploadStatus = null;
|
||||
do
|
||||
{
|
||||
queue.ForEach(log => log.Status = LogStatus.Uploading);
|
||||
LogContainer container = new(queue);
|
||||
|
||||
LogUploadResult? response = await httpClient
|
||||
.TryCatchPostAsJsonAsync<LogContainer, LogUploadResult>(API, container, options)
|
||||
.ConfigureAwait(false);
|
||||
uploadStatus = response?.Status;
|
||||
}
|
||||
while (uploadStatus != "Success");
|
||||
|
||||
queue.RemoveAll(log => log.Status == LogStatus.Uploading);
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
sessionID = Guid.NewGuid();
|
||||
queue.Add(new StartServiceLog("Analytics", "Crashes").Initialize(sessionID, deviceInfo));
|
||||
queue.Add(new StartSessionLog().Initialize(sessionID, deviceInfo).Initialize(sessionID, deviceInfo));
|
||||
}
|
||||
|
||||
public void TrackCrash(Exception exception, bool isFatal = true)
|
||||
{
|
||||
queue.Add(new ManagedErrorLog(exception, isFatal).Initialize(sessionID, deviceInfo));
|
||||
}
|
||||
|
||||
public void TrackError(Exception exception)
|
||||
{
|
||||
queue.Add(new HandledErrorLog(exception).Initialize(sessionID, deviceInfo));
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD002")]
|
||||
public void Dispose()
|
||||
{
|
||||
uploadTaskCancllationTokenSource.Cancel();
|
||||
uploadTaskCompletionSource.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.Win32;
|
||||
using Windows.Graphics;
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter;
|
||||
|
||||
/// <summary>
|
||||
/// 设备帮助类
|
||||
/// </summary>
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public static class DeviceHelper
|
||||
{
|
||||
private static readonly RegistryKey? BiosKey = Registry.LocalMachine.OpenSubKey("HARDWARE\\DESCRIPTION\\System\\BIOS");
|
||||
private static readonly RegistryKey? GeoKey = Registry.CurrentUser.OpenSubKey("Control Panel\\International\\Geo");
|
||||
private static readonly RegistryKey? CurrentVersionKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion");
|
||||
|
||||
public static string? GetOem()
|
||||
{
|
||||
string? oem = BiosKey?.GetValue("SystemManufacturer") as string;
|
||||
return oem == "System manufacturer" ? null : oem;
|
||||
}
|
||||
|
||||
public static string? GetModel()
|
||||
{
|
||||
string? model = BiosKey?.GetValue("SystemProductName") as string;
|
||||
return model == "System Product Name" ? null : model;
|
||||
}
|
||||
|
||||
public static string GetScreenSize()
|
||||
{
|
||||
RectInt32 screen = DisplayArea.Primary.OuterBounds;
|
||||
return $"{screen.Width}x{screen.Height}";
|
||||
}
|
||||
|
||||
public static string? GetCountry()
|
||||
{
|
||||
return GeoKey?.GetValue("Name") as string;
|
||||
}
|
||||
|
||||
public static string GetSystemVersion()
|
||||
{
|
||||
object? majorVersion = CurrentVersionKey?.GetValue("CurrentMajorVersionNumber");
|
||||
if (majorVersion != null)
|
||||
{
|
||||
object? minorVersion = CurrentVersionKey?.GetValue("CurrentMinorVersionNumber", "0");
|
||||
object? buildNumber = CurrentVersionKey?.GetValue("CurrentBuildNumber", "0");
|
||||
return $"{majorVersion}.{minorVersion}.{buildNumber}";
|
||||
}
|
||||
else
|
||||
{
|
||||
object? version = CurrentVersionKey?.GetValue("CurrentVersion", "0.0");
|
||||
object? buildNumber = CurrentVersionKey?.GetValue("CurrentBuild", "0");
|
||||
return $"{version}.{buildNumber}";
|
||||
}
|
||||
}
|
||||
|
||||
public static int GetSystemBuild()
|
||||
{
|
||||
return (int)(CurrentVersionKey?.GetValue("UBR") ?? 0);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public class AppCenterException
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "UnknownType";
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string? Message { get; set; }
|
||||
|
||||
[JsonPropertyName("stackTrace")]
|
||||
public string? StackTrace { get; set; }
|
||||
|
||||
[JsonPropertyName("innerExceptions")]
|
||||
public List<AppCenterException>? InnerExceptions { get; set; }
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public class Device
|
||||
{
|
||||
[JsonPropertyName("sdkName")]
|
||||
public string SdkName { get; set; } = "appcenter.winui";
|
||||
|
||||
[JsonPropertyName("sdkVersion")]
|
||||
public string SdkVersion { get; set; } = "4.5.0";
|
||||
|
||||
[JsonPropertyName("osName")]
|
||||
public string OsName { get; set; } = "WINDOWS";
|
||||
|
||||
[JsonPropertyName("osVersion")]
|
||||
public string OsVersion { get; set; } = DeviceHelper.GetSystemVersion();
|
||||
|
||||
[JsonPropertyName("osBuild")]
|
||||
public string OsBuild { get; set; } = $"{DeviceHelper.GetSystemVersion()}.{DeviceHelper.GetSystemBuild()}";
|
||||
|
||||
[JsonPropertyName("model")]
|
||||
public string? Model { get; set; } = DeviceHelper.GetModel();
|
||||
|
||||
[JsonPropertyName("oemName")]
|
||||
public string? OemName { get; set; } = DeviceHelper.GetOem();
|
||||
|
||||
[JsonPropertyName("screenSize")]
|
||||
public string ScreenSize { get; set; } = DeviceHelper.GetScreenSize();
|
||||
|
||||
[JsonPropertyName("carrierCountry")]
|
||||
public string Country { get; set; } = DeviceHelper.GetCountry() ?? "CN";
|
||||
|
||||
[JsonPropertyName("locale")]
|
||||
public string Locale { get; set; } = CultureInfo.CurrentCulture.Name;
|
||||
|
||||
[JsonPropertyName("timeZoneOffset")]
|
||||
public int TimeZoneOffset { get; set; } = (int)TimeZoneInfo.Local.BaseUtcOffset.TotalMinutes;
|
||||
|
||||
[JsonPropertyName("appVersion")]
|
||||
public string AppVersion { get; set; } = CoreEnvironment.Version.ToString();
|
||||
|
||||
[JsonPropertyName("appBuild")]
|
||||
public string AppBuild { get; set; } = CoreEnvironment.Version.ToString();
|
||||
|
||||
[JsonPropertyName("appNamespace")]
|
||||
public string AppNamespace { get; set; } = typeof(App).Namespace ?? string.Empty;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public class EventLog : PropertiesLog
|
||||
{
|
||||
public EventLog(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public override string Type { get => "event"; }
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public class HandledErrorLog : PropertiesLog
|
||||
{
|
||||
public HandledErrorLog(Exception exception)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
Exception = LogHelper.Create(exception);
|
||||
}
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
[JsonPropertyName("exception")]
|
||||
public AppCenterException Exception { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public override string Type { get => "handledError"; }
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public abstract class Log
|
||||
{
|
||||
[JsonIgnore]
|
||||
public LogStatus Status { get; set; } = LogStatus.Pending;
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public abstract string Type { get; }
|
||||
|
||||
[JsonPropertyName("sid")]
|
||||
public Guid Session { get; set; }
|
||||
|
||||
[JsonPropertyName("timestamp")]
|
||||
public string Timestamp { get; set; } = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssZ");
|
||||
|
||||
[JsonPropertyName("device")]
|
||||
public Device Device { get; set; } = default!;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public class LogContainer
|
||||
{
|
||||
public LogContainer(IEnumerable<Log> logs)
|
||||
{
|
||||
Logs = logs;
|
||||
}
|
||||
|
||||
[JsonPropertyName("logs")]
|
||||
public IEnumerable<Log> Logs { get; set; }
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
||||
|
||||
/// <summary>
|
||||
/// 日志转换器
|
||||
/// </summary>
|
||||
public class LogConverter : JsonConverter<Log>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override Log? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, Log value, JsonSerializerOptions options)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value, value.GetType(), options);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public static class LogHelper
|
||||
{
|
||||
public static T Initialize<T>(this T log, Guid sid, Device device)
|
||||
where T : Log
|
||||
{
|
||||
log.Session = sid;
|
||||
log.Device = device;
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
public static AppCenterException Create(Exception exception)
|
||||
{
|
||||
AppCenterException current = new()
|
||||
{
|
||||
Type = exception.GetType().ToString(),
|
||||
Message = exception.Message,
|
||||
StackTrace = exception.StackTrace,
|
||||
};
|
||||
|
||||
if (exception is AggregateException aggregateException)
|
||||
{
|
||||
if (aggregateException.InnerExceptions.Count != 0)
|
||||
{
|
||||
current.InnerExceptions = new();
|
||||
foreach (var innerException in aggregateException.InnerExceptions)
|
||||
{
|
||||
current.InnerExceptions.Add(Create(innerException));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exception.InnerException != null)
|
||||
{
|
||||
current.InnerExceptions ??= new();
|
||||
current.InnerExceptions.Add(Create(exception.InnerException));
|
||||
}
|
||||
|
||||
StackTrace stackTrace = new(exception, true);
|
||||
StackFrame[] frames = stackTrace.GetFrames();
|
||||
|
||||
if (frames.Length > 0 && frames[0].HasNativeImage())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
[SuppressMessage("", "SA1602")]
|
||||
public enum LogStatus
|
||||
{
|
||||
Pending,
|
||||
Uploading,
|
||||
Uploaded,
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public class ManagedErrorLog : Log
|
||||
{
|
||||
public ManagedErrorLog(Exception exception, bool fatal = true)
|
||||
{
|
||||
var p = Process.GetCurrentProcess();
|
||||
Id = Guid.NewGuid();
|
||||
Fatal = fatal;
|
||||
UserId = CoreEnvironment.AppCenterDeviceId;
|
||||
ProcessId = p.Id;
|
||||
Exception = LogHelper.Create(exception);
|
||||
ProcessName = p.ProcessName;
|
||||
Architecture = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE");
|
||||
AppLaunchTimestamp = p.StartTime.ToUniversalTime();
|
||||
}
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[JsonPropertyName("userId")]
|
||||
public string? UserId { get; set; }
|
||||
|
||||
[JsonPropertyName("processId")]
|
||||
public int ProcessId { get; set; }
|
||||
|
||||
[JsonPropertyName("processName")]
|
||||
public string ProcessName { get; set; }
|
||||
|
||||
[JsonPropertyName("fatal")]
|
||||
public bool Fatal { get; set; }
|
||||
|
||||
[JsonPropertyName("appLaunchTimestamp")]
|
||||
public DateTime? AppLaunchTimestamp { get; set; }
|
||||
|
||||
[JsonPropertyName("architecture")]
|
||||
public string? Architecture { get; set; }
|
||||
|
||||
[JsonPropertyName("exception")]
|
||||
public AppCenterException Exception { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public override string Type { get => "managedError"; }
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public class PageLog : PropertiesLog
|
||||
{
|
||||
public PageLog(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public override string Type { get => "page"; }
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public abstract class PropertiesLog : Log
|
||||
{
|
||||
[JsonPropertyName("properties")]
|
||||
public IDictionary<string, string> Properties { get; set; } = new Dictionary<string, string>();
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public class StartServiceLog : Log
|
||||
{
|
||||
public StartServiceLog(params string[] services)
|
||||
{
|
||||
Services = services;
|
||||
}
|
||||
|
||||
[JsonPropertyName("services")]
|
||||
public string[] Services { get; set; }
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public override string Type { get => "startService"; }
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model.Log;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public class StartSessionLog : Log
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public override string Type { get => "startSession"; }
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.AppCenter.Model;
|
||||
|
||||
[SuppressMessage("", "SA1600")]
|
||||
public class LogUploadResult
|
||||
{
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("validDiagnosticsIds")]
|
||||
public List<Guid> ValidDiagnosticsIds { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("throttledDiagnosticsIds")]
|
||||
public List<Guid> ThrottledDiagnosticsIds { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("correlationId")]
|
||||
public Guid CorrelationId { get; set; }
|
||||
}
|
||||
@@ -2,10 +2,12 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Context.Database;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Web.Enka;
|
||||
@@ -51,11 +53,14 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
/// <inheritdoc/>
|
||||
public async Task<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(PlayerUid uid, RefreshOption refreshOption, CancellationToken token = default)
|
||||
{
|
||||
if (await metadataService.InitializeAsync(token).ConfigureAwait(false))
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (HasOption(refreshOption, RefreshOption.RequestFromAPI))
|
||||
{
|
||||
EnkaResponse? resp = await GetEnkaResponseAsync(uid, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (resp == null)
|
||||
{
|
||||
return new(RefreshResult.APIUnavailable, null);
|
||||
@@ -67,7 +72,8 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
? UpdateDbAvatarInfo(uid.Value, resp.AvatarInfoList)
|
||||
: resp.AvatarInfoList;
|
||||
|
||||
Summary summary = await GetSummaryCoreAsync(resp.PlayerInfo, list).ConfigureAwait(false);
|
||||
Summary summary = await GetSummaryCoreAsync(resp.PlayerInfo, list, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
return new(RefreshResult.Ok, summary);
|
||||
}
|
||||
else
|
||||
@@ -79,7 +85,8 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
{
|
||||
PlayerInfo info = PlayerInfo.CreateEmpty(uid.Value);
|
||||
|
||||
Summary summary = await GetSummaryCoreAsync(info, GetDbAvatarInfos(uid.Value)).ConfigureAwait(false);
|
||||
Summary summary = await GetSummaryCoreAsync(info, GetDbAvatarInfos(uid.Value), token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
return new(RefreshResult.Ok, summary);
|
||||
}
|
||||
}
|
||||
@@ -94,10 +101,10 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
return (source & define) == define;
|
||||
}
|
||||
|
||||
private async Task<Summary> GetSummaryCoreAsync(PlayerInfo info, IEnumerable<Web.Enka.Model.AvatarInfo> avatarInfos)
|
||||
private async Task<Summary> GetSummaryCoreAsync(PlayerInfo info, IEnumerable<Web.Enka.Model.AvatarInfo> avatarInfos, CancellationToken token)
|
||||
{
|
||||
ValueStopwatch stopwatch = ValueStopwatch.StartNew();
|
||||
Summary summary = await summaryFactory.CreateAsync(info, avatarInfos).ConfigureAwait(false);
|
||||
Summary summary = await summaryFactory.CreateAsync(info, avatarInfos, token).ConfigureAwait(false);
|
||||
logger.LogInformation(EventIds.AvatarInfoGeneration, "AvatarInfoSummary Generation toke {time} ms.", stopwatch.GetElapsedTime().TotalMilliseconds);
|
||||
|
||||
return summary;
|
||||
@@ -117,7 +124,7 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
|
||||
foreach (Web.Enka.Model.AvatarInfo webInfo in webInfos)
|
||||
{
|
||||
if (webInfo.AvatarId == 10000005 || webInfo.AvatarId == 10000007)
|
||||
if (AvatarIds.IsPlayer(webInfo.AvatarId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -127,28 +134,31 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
if (entity == null)
|
||||
{
|
||||
entity = Model.Entity.AvatarInfo.Create(uid, webInfo);
|
||||
appDbContext.Add(entity);
|
||||
appDbContext.AvatarInfos.AddAndSave(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Info = webInfo;
|
||||
appDbContext.Update(entity);
|
||||
appDbContext.AvatarInfos.UpdateAndSave(entity);
|
||||
}
|
||||
}
|
||||
|
||||
appDbContext.SaveChanges();
|
||||
|
||||
return GetDbAvatarInfos(uid);
|
||||
}
|
||||
|
||||
private List<Web.Enka.Model.AvatarInfo> GetDbAvatarInfos(string uid)
|
||||
{
|
||||
return appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.Select(i => i.Info)
|
||||
|
||||
// .AsEnumerable()
|
||||
// .OrderByDescending(i => i.AvatarId)
|
||||
.ToList();
|
||||
try
|
||||
{
|
||||
return appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.Select(i => i.Info)
|
||||
.ToList();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// appDbContext can be disposed unexpectedly
|
||||
return new();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ internal interface ISummaryFactory
|
||||
/// </summary>
|
||||
/// <param name="playerInfo">玩家信息</param>
|
||||
/// <param name="avatarInfos">角色列表</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>简述对象</returns>
|
||||
Task<Summary> CreateAsync(PlayerInfo playerInfo, IEnumerable<Web.Enka.Model.AvatarInfo> avatarInfos);
|
||||
Task<Summary> CreateAsync(PlayerInfo playerInfo, IEnumerable<Web.Enka.Model.AvatarInfo> avatarInfos, CancellationToken token);
|
||||
}
|
||||
@@ -31,15 +31,15 @@ internal class SummaryFactory : ISummaryFactory
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Summary> CreateAsync(ModelPlayerInfo playerInfo, IEnumerable<ModelAvatarInfo> avatarInfos)
|
||||
public async Task<Summary> CreateAsync(ModelPlayerInfo playerInfo, IEnumerable<ModelAvatarInfo> avatarInfos, CancellationToken token)
|
||||
{
|
||||
Dictionary<int, MetadataAvatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
|
||||
Dictionary<int, MetadataWeapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
|
||||
Dictionary<int, FightProperty> idRelicMainPropMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync().ConfigureAwait(false);
|
||||
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap = await metadataService.GetIdReliquaryAffixMapAsync().ConfigureAwait(false);
|
||||
Dictionary<int, MetadataAvatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
Dictionary<int, MetadataWeapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
Dictionary<int, FightProperty> idRelicMainPropMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync(token).ConfigureAwait(false);
|
||||
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap = await metadataService.GetIdReliquaryAffixMapAsync(token).ConfigureAwait(false);
|
||||
|
||||
List<ReliquaryLevel> reliqueryLevels = await metadataService.GetReliquaryLevelsAsync().ConfigureAwait(false);
|
||||
List<MetadataReliquary> reliquaries = await metadataService.GetReliquariesAsync().ConfigureAwait(false);
|
||||
List<ReliquaryLevel> reliqueryLevels = await metadataService.GetReliquaryLevelsAsync(token).ConfigureAwait(false);
|
||||
List<MetadataReliquary> reliquaries = await metadataService.GetReliquariesAsync(token).ConfigureAwait(false);
|
||||
|
||||
SummaryFactoryImplementation inner = new(idAvatarMap, idWeaponMap, idRelicMainPropMap, idReliquaryAffixMap, reliqueryLevels, reliquaries);
|
||||
return inner.Create(playerInfo, avatarInfos);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
using Snap.Hutao.Model.Intrinsic;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Model.Metadata.Reliquary;
|
||||
using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
|
||||
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
|
||||
@@ -60,7 +61,7 @@ internal class SummaryFactoryImplementation
|
||||
return new()
|
||||
{
|
||||
Player = SummaryHelper.CreatePlayer(playerInfo),
|
||||
Avatars = avatarInfos.Select(a =>
|
||||
Avatars = avatarInfos.Where(a => !AvatarIds.IsPlayer(a.AvatarId)).Select(a =>
|
||||
{
|
||||
SummaryAvatarFactory summaryAvatarFactory = new(
|
||||
idAvatarMap,
|
||||
|
||||
@@ -209,7 +209,8 @@ internal static class SummaryHelper
|
||||
(2, 0) => 100,
|
||||
(2, 1) => 80,
|
||||
|
||||
_ => throw Must.NeverHappen(),
|
||||
// TODO: Not quite sure why can we hit this branch.
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -226,16 +227,6 @@ internal static class SummaryHelper
|
||||
return 100 * ((cr * 2) + cd);
|
||||
}
|
||||
|
||||
private static string FormatValue(FormatMethod method, double value)
|
||||
{
|
||||
return method switch
|
||||
{
|
||||
FormatMethod.Integer => Math.Round((double)value, MidpointRounding.AwayFromZero).ToString(),
|
||||
FormatMethod.Percent => string.Format("{0:P1}", value),
|
||||
_ => value.ToString(),
|
||||
};
|
||||
}
|
||||
|
||||
private static FightProperty GetBonusFightProperty(IDictionary<FightProperty, double> fightPropMap)
|
||||
{
|
||||
if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY))
|
||||
|
||||
@@ -123,15 +123,15 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeAsync(CancellationToken token = default)
|
||||
public async ValueTask<bool> InitializeAsync()
|
||||
{
|
||||
if (await metadataService.InitializeAsync(token).ConfigureAwait(false))
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
nameAvatarMap = await metadataService.GetNameToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
nameWeaponMap = await metadataService.GetNameToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
nameAvatarMap = await metadataService.GetNameToAvatarMapAsync().ConfigureAwait(false);
|
||||
nameWeaponMap = await metadataService.GetNameToWeaponMapAsync().ConfigureAwait(false);
|
||||
|
||||
idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
|
||||
idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
|
||||
|
||||
IsInitialized = true;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ internal interface IGachaLogService
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>是否初始化成功</returns>
|
||||
ValueTask<bool> InitializeAsync(CancellationToken token = default);
|
||||
ValueTask<bool> InitializeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 刷新祈愿记录
|
||||
|
||||
@@ -54,59 +54,34 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
|
||||
{
|
||||
using (FileStream fileStream = new(tempFile.Path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
using (BinaryReader reader = new(fileStream))
|
||||
using (MemoryStream memoryStream = new())
|
||||
{
|
||||
string url = string.Empty;
|
||||
while (!reader.EndOfStream())
|
||||
{
|
||||
uint test = reader.ReadUInt32();
|
||||
|
||||
if (test == 0x2F302F31)
|
||||
{
|
||||
byte[] chars = ReadBytesUntilZero(reader);
|
||||
string result = Encoding.UTF8.GetString(chars.AsSpan());
|
||||
|
||||
if (result.Contains("&auth_appid=webview_gacha"))
|
||||
{
|
||||
url = result;
|
||||
}
|
||||
|
||||
// align up
|
||||
long offset = reader.BaseStream.Position % 128;
|
||||
reader.BaseStream.Position += 128 - offset;
|
||||
}
|
||||
}
|
||||
|
||||
return new(!string.IsNullOrEmpty(url), url);
|
||||
await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||
string? result = Match(memoryStream);
|
||||
return new(!string.IsNullOrEmpty(result), result!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(false, $"未正确提供原神路径,或当前设置的路径不正确");
|
||||
return new(false, "未正确提供原神路径,或当前设置的路径不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] ReadBytesUntilZero(BinaryReader binaryReader)
|
||||
private static string? Match(MemoryStream stream)
|
||||
{
|
||||
return ReadByteEnumerableUntilZero(binaryReader).ToArray();
|
||||
}
|
||||
ReadOnlySpan<byte> span = stream.ToArray();
|
||||
ReadOnlySpan<byte> match = Encoding.UTF8.GetBytes("https://webstatic.mihoyo.com/hk4e/event/e20190909gacha-v2/index.html");
|
||||
ReadOnlySpan<byte> zero = Encoding.UTF8.GetBytes("\0");
|
||||
|
||||
private static IEnumerable<byte> ReadByteEnumerableUntilZero(BinaryReader binaryReader)
|
||||
{
|
||||
while (binaryReader.BaseStream.Position < binaryReader.BaseStream.Length)
|
||||
int index = span.LastIndexOf(match);
|
||||
if (index >= 0)
|
||||
{
|
||||
byte b = binaryReader.ReadByte();
|
||||
|
||||
if (b == 0x00)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return b;
|
||||
}
|
||||
int length = span[index..].IndexOf(zero);
|
||||
return Encoding.UTF8.GetString(span.Slice(index, length));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Win32;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
|
||||
/// <summary>
|
||||
/// 定义了对注册表的操作
|
||||
/// </summary>
|
||||
internal static class GameAccountRegistryInterop
|
||||
{
|
||||
private const string GenshinKey = @"HKEY_CURRENT_USER\Software\miHoYo\原神";
|
||||
private const string SdkKey = "MIHOYOSDK_ADL_PROD_CN_h3123967166";
|
||||
|
||||
/// <summary>
|
||||
/// 设置键值
|
||||
/// </summary>
|
||||
/// <param name="account">账户</param>
|
||||
/// <returns>账号是否设置</returns>
|
||||
public static bool Set(GameAccount? account)
|
||||
{
|
||||
if (account != null)
|
||||
{
|
||||
Registry.SetValue(GenshinKey, SdkKey, Encoding.UTF8.GetBytes(account.MihoyoSDK));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在注册表中获取账号信息
|
||||
/// </summary>
|
||||
/// <returns>当前注册表中的信息</returns>
|
||||
public static string? Get()
|
||||
{
|
||||
object? sdk = Registry.GetValue(GenshinKey, SdkKey, Array.Empty<byte>());
|
||||
|
||||
if (sdk is byte[] bytes)
|
||||
{
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Snap.Hutao.Context.Database;
|
||||
@@ -8,9 +9,11 @@ using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.IO.Ini;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Model.Binding.LaunchGame;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service.Game.Locator;
|
||||
using Snap.Hutao.Service.Game.Unlocker;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
@@ -24,6 +27,7 @@ namespace Snap.Hutao.Service.Game;
|
||||
internal class GameService : IGameService
|
||||
{
|
||||
private const string GamePathKey = $"{nameof(GameService)}.Cache.{SettingEntry.GamePath}";
|
||||
private const string ConfigFile = "config.ini";
|
||||
|
||||
private readonly IServiceScopeFactory scopeFactory;
|
||||
private readonly IMemoryCache memoryCache;
|
||||
@@ -56,7 +60,7 @@ internal class GameService : IGameService
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
SettingEntry entry = appDbContext.Settings.SingleOrAdd(e => e.Key == SettingEntry.GamePath, () => new(SettingEntry.GamePath, null), out bool added);
|
||||
SettingEntry entry = appDbContext.Settings.SingleOrAdd(e => e.Key == SettingEntry.GamePath, () => new(SettingEntry.GamePath, string.Empty), out bool added);
|
||||
|
||||
// Cannot find in setting
|
||||
if (added)
|
||||
@@ -86,6 +90,11 @@ internal class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.Value == null)
|
||||
{
|
||||
return new(false, null!);
|
||||
}
|
||||
|
||||
// Set cache and return.
|
||||
string path = memoryCache.Set(GamePathKey, Must.NotNull(entry.Value!));
|
||||
return new(true, path);
|
||||
@@ -136,7 +145,7 @@ internal class GameService : IGameService
|
||||
public MultiChannel GetMultiChannel()
|
||||
{
|
||||
string gamePath = GetGamePathSkipLocator();
|
||||
string configPath = Path.Combine(gamePath, "config.ini");
|
||||
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFile);
|
||||
|
||||
using (FileStream stream = File.OpenRead(configPath))
|
||||
{
|
||||
@@ -148,11 +157,61 @@ internal class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ObservableCollection<GameAccount>> GetGameAccountCollectionAsync()
|
||||
/// <inheritdoc/>
|
||||
public void SetMultiChannel(LaunchScheme scheme)
|
||||
{
|
||||
string gamePath = GetGamePathSkipLocator();
|
||||
string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFile);
|
||||
|
||||
List<IniElement> elements;
|
||||
using (FileStream readStream = File.OpenRead(configPath))
|
||||
{
|
||||
elements = IniSerializer.Deserialize(readStream).ToList();
|
||||
}
|
||||
|
||||
foreach (IniElement element in elements)
|
||||
{
|
||||
if (element is IniParameter parameter)
|
||||
{
|
||||
if (parameter.Key == "channel")
|
||||
{
|
||||
parameter.Value = scheme.Channel;
|
||||
}
|
||||
|
||||
if (parameter.Key == "sub_channel")
|
||||
{
|
||||
parameter.Value = scheme.SubChannel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (FileStream writeStream = File.Create(configPath))
|
||||
{
|
||||
IniSerializer.Serialize(writeStream, elements);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsGameRunning()
|
||||
{
|
||||
if (gameSemaphore.CurrentCount == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Process.GetProcessesByName("YuanShen.exe").Any();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableCollection<GameAccount> GetGameAccountCollection()
|
||||
{
|
||||
if (gameAccounts == null)
|
||||
{
|
||||
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
gameAccounts = new(appDbContext.GameAccounts.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
return gameAccounts;
|
||||
@@ -161,19 +220,24 @@ internal class GameService : IGameService
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask LaunchAsync(LaunchConfiguration configuration)
|
||||
{
|
||||
if (gameSemaphore.CurrentCount == 0)
|
||||
if (IsGameRunning())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string gamePath = GetGamePathSkipLocator();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(gamePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html
|
||||
string commandLine = new CommandLineBuilder()
|
||||
.AppendIf("-popupwindow", configuration.IsBorderless)
|
||||
.Append("-screen-fullscreen", configuration.IsFullScreen ? 1 : 0)
|
||||
.Append("-screen-width", configuration.ScreenWidth)
|
||||
.Append("-screen-height", configuration.ScreenHeight)
|
||||
.Append("-monitor", configuration.Monitor)
|
||||
.Build();
|
||||
|
||||
Process game = new()
|
||||
@@ -190,22 +254,121 @@ internal class GameService : IGameService
|
||||
|
||||
using (await gameSemaphore.EnterAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (configuration.UnlockFPS)
|
||||
try
|
||||
{
|
||||
IGameFpsUnlocker unlocker = new GameFpsUnlocker(game, configuration.TargetFPS);
|
||||
|
||||
TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100);
|
||||
TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000);
|
||||
TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(2000);
|
||||
await unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (game.Start())
|
||||
if (configuration.UnlockFPS)
|
||||
{
|
||||
await game.WaitForExitAsync().ConfigureAwait(false);
|
||||
IGameFpsUnlocker unlocker = new GameFpsUnlocker(game, configuration.TargetFPS);
|
||||
|
||||
TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100);
|
||||
TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000);
|
||||
TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(2000);
|
||||
if (game.Start())
|
||||
{
|
||||
await unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (game.Start())
|
||||
{
|
||||
await game.WaitForExitAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Win32Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DetectGameAccountAsync()
|
||||
{
|
||||
Must.NotNull(gameAccounts!);
|
||||
|
||||
string? registrySdk = GameAccountRegistryInterop.Get();
|
||||
if (!string.IsNullOrEmpty(registrySdk))
|
||||
{
|
||||
GameAccount? account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
|
||||
|
||||
if (account == null)
|
||||
{
|
||||
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
(bool isOk, string name) = await new GameAccountNameDialog(mainWindow).GetInputNameAsync().ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
account = GameAccount.Create(name, registrySdk);
|
||||
|
||||
// sync cache
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
gameAccounts.Add(GameAccount.Create(name, registrySdk));
|
||||
|
||||
// sync database
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
appDbContext.GameAccounts.AddAndSave(account);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SetGameAccount(GameAccount account)
|
||||
{
|
||||
return GameAccountRegistryInterop.Set(account);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
|
||||
{
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
gameAccount.UpdateAttachUid(uid);
|
||||
appDbContext.GameAccounts.UpdateAndSave(gameAccount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
|
||||
{
|
||||
MainWindow mainWindow = Ioc.Default.GetRequiredService<MainWindow>();
|
||||
(bool isOk, string name) = await new GameAccountNameDialog(mainWindow).GetInputNameAsync().ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
gameAccount.UpdateName(name);
|
||||
|
||||
// sync database
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
appDbContext.GameAccounts.UpdateAndSave(gameAccount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
|
||||
{
|
||||
Must.NotNull(gameAccounts!).Remove(gameAccount);
|
||||
|
||||
await Task.Yield();
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
try
|
||||
{
|
||||
appDbContext.GameAccounts.RemoveAndSave(gameAccount);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
// This gameAccount has already been deleted.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Model.Binding.LaunchGame;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
|
||||
@@ -10,6 +13,26 @@ namespace Snap.Hutao.Service.Game;
|
||||
/// </summary>
|
||||
internal interface IGameService
|
||||
{
|
||||
/// <summary>
|
||||
/// 将账号绑定到对应的Uid
|
||||
/// 清除老账号的绑定状态
|
||||
/// </summary>
|
||||
/// <param name="gameAccount">游戏内账号</param>
|
||||
/// <param name="uid">uid</param>
|
||||
void AttachGameAccountToUid(GameAccount gameAccount, string uid);
|
||||
|
||||
/// <summary>
|
||||
/// 检测并尝试添加游戏内账户
|
||||
/// </summary>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask DetectGameAccountAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取游戏内账号集合
|
||||
/// </summary>
|
||||
/// <returns>游戏内账号集合</returns>
|
||||
ObservableCollection<GameAccount> GetGameAccountCollection();
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取游戏路径
|
||||
/// </summary>
|
||||
@@ -28,6 +51,12 @@ internal interface IGameService
|
||||
/// <returns>多通道值</returns>
|
||||
MultiChannel GetMultiChannel();
|
||||
|
||||
/// <summary>
|
||||
/// 游戏是否正在运行
|
||||
/// </summary>
|
||||
/// <returns>是否正在运行</returns>
|
||||
bool IsGameRunning();
|
||||
|
||||
/// <summary>
|
||||
/// 异步启动
|
||||
/// </summary>
|
||||
@@ -35,9 +64,36 @@ internal interface IGameService
|
||||
/// <returns>任务</returns>
|
||||
ValueTask LaunchAsync(LaunchConfiguration configuration);
|
||||
|
||||
/// <summary>
|
||||
/// 异步修改游戏账号名称
|
||||
/// </summary>
|
||||
/// <param name="gameAccount">游戏账号</param>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask ModifyGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
/// <summary>
|
||||
/// 重写游戏路径
|
||||
/// </summary>
|
||||
/// <param name="path">路径</param>
|
||||
void OverwriteGamePath(string path);
|
||||
|
||||
/// <summary>
|
||||
/// 异步尝试移除账号
|
||||
/// </summary>
|
||||
/// <param name="gameAccount">账号</param>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
/// <summary>
|
||||
/// 修改注册表中的账号信息
|
||||
/// </summary>
|
||||
/// <param name="account">账号</param>
|
||||
/// <returns>是否设置成功</returns>
|
||||
bool SetGameAccount(GameAccount account);
|
||||
|
||||
/// <summary>
|
||||
/// 设置多通道值
|
||||
/// </summary>
|
||||
/// <param name="scheme">方案</param>
|
||||
void SetMultiChannel(LaunchScheme scheme);
|
||||
}
|
||||
@@ -6,40 +6,55 @@ namespace Snap.Hutao.Service.Game;
|
||||
/// <summary>
|
||||
/// 启动游戏配置
|
||||
/// </summary>
|
||||
internal struct LaunchConfiguration
|
||||
internal readonly struct LaunchConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否全屏,全屏时无边框设置将被覆盖
|
||||
/// </summary>
|
||||
public bool IsFullScreen { get; set; }
|
||||
public readonly bool IsFullScreen;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为无边框窗口
|
||||
/// </summary>
|
||||
public bool IsBorderless { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用解锁帧率
|
||||
/// </summary>
|
||||
public bool UnlockFPS { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标帧率
|
||||
/// </summary>
|
||||
public int TargetFPS { get; private set; }
|
||||
public readonly bool IsBorderless;
|
||||
|
||||
/// <summary>
|
||||
/// 窗口宽度
|
||||
/// </summary>
|
||||
public int ScreenWidth { get; private set; }
|
||||
public readonly int ScreenWidth;
|
||||
|
||||
/// <summary>
|
||||
/// 窗口高度
|
||||
/// </summary>
|
||||
public int ScreenHeight { get; private set; }
|
||||
public readonly int ScreenHeight;
|
||||
|
||||
/// <summary>
|
||||
/// 显示器编号
|
||||
/// 是否启用解锁帧率
|
||||
/// </summary>
|
||||
public int Monitor { get; private set; }
|
||||
public readonly bool UnlockFPS;
|
||||
|
||||
/// <summary>
|
||||
/// 目标帧率
|
||||
/// </summary>
|
||||
public readonly int TargetFPS;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的启动配置
|
||||
/// </summary>
|
||||
/// <param name="isFullScreen">全屏</param>
|
||||
/// <param name="isBorderless">无边框</param>
|
||||
/// <param name="screenWidth">宽度</param>
|
||||
/// <param name="screenHeight">高度</param>
|
||||
/// <param name="unlockFps">解锁帧率</param>
|
||||
/// <param name="targetFps">目标帧率</param>
|
||||
public LaunchConfiguration(bool isFullScreen, bool isBorderless, int screenWidth, int screenHeight, bool unlockFps, int targetFps)
|
||||
{
|
||||
IsFullScreen = isFullScreen;
|
||||
IsBorderless = isBorderless;
|
||||
ScreenHeight = screenHeight;
|
||||
ScreenWidth = screenWidth;
|
||||
ScreenHeight = screenHeight;
|
||||
UnlockFPS = unlockFps;
|
||||
TargetFPS = targetFps;
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,8 @@ internal class ManualGameLocator : IGameLocator
|
||||
picker.FileTypeFilter.Add(".exe");
|
||||
picker.SuggestedStartLocation = PickerLocationId.ComputerFolder;
|
||||
|
||||
// System.Runtime.InteropServices.COMException (0x80004005): Error HRESULT E_FAIL has been returned from a call to a COM component.
|
||||
// Not sure what's going on here.
|
||||
if (await picker.PickSingleFileAsync() is StorageFile file)
|
||||
{
|
||||
string path = file.Path;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Binding.Hutao;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Weapon;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
@@ -111,8 +112,8 @@ internal class HutaoCache : IHutaoCache
|
||||
Dictionary<int, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
|
||||
idAvatarExtendedMap = new(idAvatarMap)
|
||||
{
|
||||
[10000005] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerBoy", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
|
||||
[10000007] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerGirl", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
|
||||
[AvatarIds.PlayerBoy] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerBoy", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
|
||||
[AvatarIds.PlayerGirl] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerGirl", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,8 @@ internal interface IMetadataService
|
||||
/// <summary>
|
||||
/// 异步初始化服务,尝试更新元数据
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>初始化是否成功</returns>
|
||||
ValueTask<bool> InitializeAsync(CancellationToken token = default);
|
||||
ValueTask<bool> InitializeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取成就列表
|
||||
|
||||
@@ -69,7 +69,7 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
|
||||
public bool IsInitialized { get => isInitialized; private set => isInitialized = value; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeAsync(CancellationToken token = default)
|
||||
public async ValueTask<bool> InitializeAsync()
|
||||
{
|
||||
await initializeCompletionSource.Task.ConfigureAwait(false);
|
||||
return IsInitialized;
|
||||
|
||||
@@ -181,13 +181,16 @@ internal class NavigationService : INavigationService
|
||||
/// <inheritdoc/>
|
||||
public void GoBack()
|
||||
{
|
||||
bool canGoBack = Frame?.CanGoBack ?? false;
|
||||
|
||||
if (canGoBack)
|
||||
Program.DispatcherQueue!.TryEnqueue(() =>
|
||||
{
|
||||
Frame!.GoBack();
|
||||
SyncSelectedNavigationViewItemWith(Frame.Content.GetType());
|
||||
}
|
||||
bool canGoBack = Frame?.CanGoBack ?? false;
|
||||
|
||||
if (canGoBack)
|
||||
{
|
||||
Frame!.GoBack();
|
||||
SyncSelectedNavigationViewItemWith(Frame.Content.GetType());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -95,7 +95,6 @@ internal class UserService : IUserService
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
appDbContext.Users.RemoveAndSave(user.Entity);
|
||||
}
|
||||
}
|
||||
@@ -162,6 +161,7 @@ internal class UserService : IUserService
|
||||
if (cookie.ContainsSToken())
|
||||
{
|
||||
// insert stoken
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
userWithSameUid.UpdateSToken(uid, cookie);
|
||||
appDbContext.Users.UpdateAndSave(userWithSameUid.Entity);
|
||||
|
||||
@@ -170,6 +170,7 @@ internal class UserService : IUserService
|
||||
|
||||
if (cookie.ContainsLTokenAndCookieToken())
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
userWithSameUid.Cookie = cookie;
|
||||
appDbContext.Users.UpdateAndSave(userWithSameUid.Entity);
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<None Remove="View\Dialog\GachaLogImportDialog.xaml" />
|
||||
<None Remove="View\Dialog\GachaLogRefreshProgressDialog.xaml" />
|
||||
<None Remove="View\Dialog\GachaLogUrlDialog.xaml" />
|
||||
<None Remove="View\Dialog\UserAutoCookieDialog.xaml" />
|
||||
<None Remove="View\Dialog\GameAccountNameDialog.xaml" />
|
||||
<None Remove="View\Dialog\UserDialog.xaml" />
|
||||
<None Remove="View\MainView.xaml" />
|
||||
<None Remove="View\Page\AchievementPage.xaml" />
|
||||
@@ -150,6 +150,11 @@
|
||||
<ItemGroup>
|
||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Dialog\GameAccountNameDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Page\LoginMihoyoBBSPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -190,11 +195,6 @@
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Dialog\UserAutoCookieDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Dialog\AchievementArchiveCreateDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<ContentDialog
|
||||
x:Class="Snap.Hutao.View.Dialog.GameAccountNameDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="为账号命名"
|
||||
DefaultButton="Primary"
|
||||
PrimaryButtonText="确认"
|
||||
CloseButtonText="取消"
|
||||
Style="{StaticResource DefaultContentDialogStyle}">
|
||||
|
||||
<Grid>
|
||||
<TextBox
|
||||
Margin="0,0,0,0"
|
||||
x:Name="InputText"
|
||||
PlaceholderText="在此处输入"
|
||||
VerticalAlignment="Top"/>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
|
||||
namespace Snap.Hutao.View.Dialog;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏账号命名对话框
|
||||
/// </summary>
|
||||
public sealed partial class GameAccountNameDialog : ContentDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的游戏账号命名对话框
|
||||
/// </summary>
|
||||
/// <param name="window">窗体</param>
|
||||
public GameAccountNameDialog(Window window)
|
||||
{
|
||||
InitializeComponent();
|
||||
XamlRoot = window.Content.XamlRoot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取输入的Cookie
|
||||
/// </summary>
|
||||
/// <returns>输入的结果</returns>
|
||||
public async Task<ValueResult<bool, string>> GetInputNameAsync()
|
||||
{
|
||||
ContentDialogResult result = await ShowAsync();
|
||||
string text = InputText.Text;
|
||||
return new(result == ContentDialogResult.Primary && (!string.IsNullOrEmpty(text)), text);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<ContentDialog
|
||||
x:Class="Snap.Hutao.View.Dialog.UserAutoCookieDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="登录米哈游通行证"
|
||||
DefaultButton="Primary"
|
||||
PrimaryButtonText="继续"
|
||||
CloseButtonText="取消"
|
||||
Style="{StaticResource DefaultContentDialogStyle}">
|
||||
<ContentDialog.Resources>
|
||||
<x:Double x:Key="ContentDialogMaxWidth">1600</x:Double>
|
||||
<x:Double x:Key="ContentDialogMinHeight">200</x:Double>
|
||||
<x:Double x:Key="ContentDialogMaxHeight">1200</x:Double>
|
||||
</ContentDialog.Resources>
|
||||
<Grid Loaded="OnRootLoaded">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Text="在下方登录"
|
||||
Grid.Row="0"/>
|
||||
<WebView2
|
||||
Grid.Row="2"
|
||||
Margin="0,12,0,0"
|
||||
Width="640"
|
||||
Height="400"
|
||||
x:Name="WebView"/>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
@@ -1,69 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
namespace Snap.Hutao.View.Dialog;
|
||||
|
||||
/// <summary>
|
||||
/// 用户自动Cookie对话框
|
||||
/// </summary>
|
||||
public sealed partial class UserAutoCookieDialog : ContentDialog
|
||||
{
|
||||
private Cookie? cookie;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的用户自动Cookie对话框
|
||||
/// </summary>
|
||||
/// <param name="window">依赖窗口</param>
|
||||
public UserAutoCookieDialog(Window window)
|
||||
{
|
||||
InitializeComponent();
|
||||
XamlRoot = window.Content.XamlRoot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取输入的Cookie
|
||||
/// </summary>
|
||||
/// <returns>输入的结果</returns>
|
||||
public async Task<ValueResult<bool, Cookie>> GetInputCookieAsync()
|
||||
{
|
||||
ContentDialogResult result = await ShowAsync();
|
||||
return new(result == ContentDialogResult.Primary && cookie != null, cookie!);
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD100")]
|
||||
private async void OnRootLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await WebView.EnsureCoreWebView2Async();
|
||||
WebView.CoreWebView2.SourceChanged += OnCoreWebView2SourceChanged;
|
||||
|
||||
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
|
||||
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
|
||||
foreach (CoreWebView2Cookie item in cookies)
|
||||
{
|
||||
manager.DeleteCookie(item);
|
||||
}
|
||||
|
||||
WebView.CoreWebView2.Navigate("https://user.mihoyo.com/#/login/password");
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD100")]
|
||||
private async void OnCoreWebView2SourceChanged(CoreWebView2 sender, CoreWebView2SourceChangedEventArgs args)
|
||||
{
|
||||
if (sender != null)
|
||||
{
|
||||
if (sender.Source.ToString() == "https://user.mihoyo.com/#/account/home")
|
||||
{
|
||||
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
|
||||
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
|
||||
cookie = Cookie.FromCoreWebView2Cookies(cookies);
|
||||
WebView.CoreWebView2.SourceChanged -= OnCoreWebView2SourceChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
xmlns:settings="using:SettingsUI.Controls"
|
||||
mc:Ignorable="d"
|
||||
IsPrimaryButtonEnabled="False"
|
||||
Title="设置米游社Cookie"
|
||||
Title="设置 Cookie"
|
||||
DefaultButton="Primary"
|
||||
PrimaryButtonText="请输入Cookie"
|
||||
CloseButtonText="取消"
|
||||
@@ -20,17 +20,19 @@
|
||||
TextChanged="InputTextChanged"
|
||||
PlaceholderText="在此处输入"
|
||||
VerticalAlignment="Top"/>
|
||||
<TextBlock Margin="0,4,0,0" Text="接受包含 Cookie / LoginTicket / Stoken 的字符串"/>
|
||||
<settings:SettingsGroup Margin="0,-48,0,0">
|
||||
|
||||
<settings:Setting
|
||||
Icon=""
|
||||
Header="操作文档"
|
||||
Description="进入我们的文档页面并按指示操作"
|
||||
Description="进入文档页面并按指示操作"
|
||||
HorizontalAlignment="Stretch">
|
||||
<HyperlinkButton
|
||||
Margin="12,0,0,0"
|
||||
Padding="6"
|
||||
Content="立即前往"
|
||||
NavigateUri="https://www.snapgenshin.com/documents/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie"/>
|
||||
NavigateUri="https://hut.ao/features/mhy-account-switch.html#%E5%A6%82%E4%BD%95%E8%8E%B7%E5%8F%96-cookie"/>
|
||||
</settings:Setting>
|
||||
</settings:SettingsGroup>
|
||||
</StackPanel>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<Thickness x:Key="PivotItemMargin">0</Thickness>
|
||||
</shc:ScopedPage.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid Visibility="{Binding IsInitialized,Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Rectangle
|
||||
Height="48"
|
||||
VerticalAlignment="Top"
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
<control:ScopedPage
|
||||
<shc:ScopedPage
|
||||
x:Class="Snap.Hutao.View.Page.LaunchGamePage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:mxic="using:Microsoft.Xaml.Interactions.Core"
|
||||
xmlns:mxim="using:Microsoft.Xaml.Interactions.Media"
|
||||
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:sc="using:SettingsUI.Controls"
|
||||
xmlns:control="using:Snap.Hutao.Control"
|
||||
xmlns:shc="using:Snap.Hutao.Control"
|
||||
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
||||
xmlns:shv="using:Snap.Hutao.ViewModel"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance shv:LaunchGameViewModel}"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
|
||||
<mxi:Interaction.Behaviors>
|
||||
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
|
||||
</mxi:Interaction.Behaviors>
|
||||
|
||||
<Page.Resources>
|
||||
<shc:BindingProxy x:Key="BindingProxy" DataContext="{Binding}"/>
|
||||
|
||||
<Style TargetType="Button" BasedOn="{StaticResource SettingButtonStyle}">
|
||||
<Setter Property="MinWidth" Value="120"/>
|
||||
<Setter Property="MinWidth" Value="160"/>
|
||||
</Style>
|
||||
<Style TargetType="HyperlinkButton" BasedOn="{StaticResource HyperlinkButtonStyle}">
|
||||
<Setter Property="MinWidth" Value="120"/>
|
||||
<Setter Property="MinWidth" Value="160"/>
|
||||
</Style>
|
||||
</Page.Resources>
|
||||
<Grid>
|
||||
@@ -34,10 +47,14 @@
|
||||
Header="服务器"
|
||||
Description="切换游戏服务器,B服用户需要自备额外的 PCGameSDK.dll 文件">
|
||||
<sc:Setting.ActionContent>
|
||||
<ComboBox Width="120"/>
|
||||
<ComboBox
|
||||
Width="160"
|
||||
ItemsSource="{Binding KnownSchemes}"
|
||||
SelectedItem="{Binding SelectedScheme,Mode=TwoWay}"
|
||||
DisplayMemberPath="Name"/>
|
||||
</sc:Setting.ActionContent>
|
||||
</sc:Setting>
|
||||
<sc:SettingExpander>
|
||||
<sc:SettingExpander IsExpanded="True">
|
||||
<sc:SettingExpander.Header>
|
||||
<Grid Padding="0,16">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
@@ -45,7 +62,6 @@
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
Margin="20,0,0,0"
|
||||
|
||||
Text="账号"/>
|
||||
<TextBlock
|
||||
Opacity="0.8"
|
||||
@@ -53,19 +69,103 @@
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="在游戏内切换账号,网络环境发生变化后需要重新手动检测"/>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<Button
|
||||
HorizontalAlignment="Right"
|
||||
Command="{Binding DetectGameAccountCommand}"
|
||||
Grid.Column="1"
|
||||
Margin="0,0,8,0"
|
||||
Width="80"
|
||||
MinWidth="88"
|
||||
Width="128"
|
||||
MinWidth="128"
|
||||
Content="检测"/>
|
||||
</Grid>
|
||||
|
||||
</sc:SettingExpander.Header>
|
||||
<ListView
|
||||
ItemsSource="{Binding GameAccounts}"
|
||||
SelectedItem="{Binding SelectedGameAccount,Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<StackPanel Margin="0,12">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
<TextBlock
|
||||
Opacity="0.8"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{Binding AttachUid,TargetNullValue=该账号尚未绑定 UID}"/>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
x:Name="ButtonPanel"
|
||||
Visibility="Collapsed"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Margin="4,8"
|
||||
MinWidth="48"
|
||||
VerticalAlignment="Stretch"
|
||||
ToolTipService.ToolTip="绑定当前用户角色"
|
||||
Content=""
|
||||
Command="{Binding DataContext.AttachGameAccountCommand,Source={StaticResource BindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"/>
|
||||
<Button
|
||||
Margin="4,8"
|
||||
MinWidth="48"
|
||||
VerticalAlignment="Stretch"
|
||||
ToolTipService.ToolTip="重命名"
|
||||
Content=""
|
||||
Command="{Binding DataContext.ModifyGameAccountCommand,Source={StaticResource BindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"/>
|
||||
<Button
|
||||
Margin="4,8"
|
||||
MinWidth="48"
|
||||
VerticalAlignment="Stretch"
|
||||
ToolTipService.ToolTip="删除"
|
||||
Content=""
|
||||
Command="{Binding DataContext.RemoveGameAccountCommand,Source={StaticResource BindingProxy}}"
|
||||
CommandParameter="{Binding}"
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Grid.Resources>
|
||||
<Storyboard x:Name="ButtonPanelVisibleStoryboard">
|
||||
<ObjectAnimationUsingKeyFrames
|
||||
Storyboard.TargetName="ButtonPanel"
|
||||
Storyboard.TargetProperty="Visibility">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<Visibility>Visible</Visibility>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
|
||||
<Storyboard x:Name="ButtonPanelCollapsedStoryboard">
|
||||
<ObjectAnimationUsingKeyFrames
|
||||
Storyboard.TargetName="ButtonPanel"
|
||||
Storyboard.TargetProperty="Visibility">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<Visibility>Collapsed</Visibility>
|
||||
</DiscreteObjectKeyFrame.Value>
|
||||
</DiscreteObjectKeyFrame>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</Grid.Resources>
|
||||
|
||||
<mxi:Interaction.Behaviors>
|
||||
<mxic:EventTriggerBehavior EventName="PointerEntered">
|
||||
<mxim:ControlStoryboardAction Storyboard="{StaticResource ButtonPanelVisibleStoryboard}"/>
|
||||
</mxic:EventTriggerBehavior>
|
||||
<mxic:EventTriggerBehavior EventName="PointerExited">
|
||||
<mxim:ControlStoryboardAction Storyboard="{StaticResource ButtonPanelCollapsedStoryboard}"/>
|
||||
</mxic:EventTriggerBehavior>
|
||||
</mxi:Interaction.Behaviors>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</sc:SettingExpander>
|
||||
</sc:SettingsGroup>
|
||||
<sc:SettingsGroup Header="外观">
|
||||
@@ -75,8 +175,9 @@
|
||||
Description="覆盖默认的全屏状态">
|
||||
<sc:Setting.ActionContent>
|
||||
<ToggleSwitch
|
||||
Style="{StaticResource ToggleSwitchSettingStyle}"
|
||||
Width="120"/>
|
||||
IsOn="{Binding IsFullScreen,Mode=TwoWay}"
|
||||
Style="{StaticResource ToggleSwitchSettingStyle}"
|
||||
Width="120"/>
|
||||
</sc:Setting.ActionContent>
|
||||
</sc:Setting>
|
||||
<sc:Setting
|
||||
@@ -85,8 +186,9 @@
|
||||
Description="将窗口创建为弹出窗口,不带框架">
|
||||
<sc:Setting.ActionContent>
|
||||
<ToggleSwitch
|
||||
Style="{StaticResource ToggleSwitchSettingStyle}"
|
||||
Width="120"/>
|
||||
IsOn="{Binding IsBorderless,Mode=TwoWay}"
|
||||
Style="{StaticResource ToggleSwitchSettingStyle}"
|
||||
Width="120"/>
|
||||
</sc:Setting.ActionContent>
|
||||
</sc:Setting>
|
||||
|
||||
@@ -97,7 +199,8 @@
|
||||
Description="覆盖默认屏幕宽度">
|
||||
<sc:Setting.ActionContent>
|
||||
<NumberBox
|
||||
Width="120"/>
|
||||
Value="{Binding ScreenWidth,Mode=TwoWay}"
|
||||
Width="160"/>
|
||||
</sc:Setting.ActionContent>
|
||||
</sc:Setting>
|
||||
<sc:Setting
|
||||
@@ -106,29 +209,32 @@
|
||||
Description="覆盖默认屏幕高度">
|
||||
<sc:Setting.ActionContent>
|
||||
<NumberBox
|
||||
Width="120"/>
|
||||
Value="{Binding ScreenHeight,Mode=TwoWay}"
|
||||
Width="160"/>
|
||||
</sc:Setting.ActionContent>
|
||||
</sc:Setting>
|
||||
</sc:SettingsGroup>
|
||||
|
||||
<sc:SettingsGroup Header="Dangerous feature">
|
||||
<sc:SettingsGroup Header="Dangerous Feature" IsEnabled="{Binding IsElevated}">
|
||||
<sc:Setting
|
||||
Icon=""
|
||||
Header="Unlock frame rate limit"
|
||||
Description="Requires administrator privilege. Otherwise the option does not take effect.">
|
||||
Description="Requires administrator privilege. Otherwise the option will be disabled.">
|
||||
<sc:Setting.ActionContent>
|
||||
<ToggleSwitch
|
||||
OnContent="Enable"
|
||||
OffContent="Disable"
|
||||
Style="{StaticResource ToggleSwitchSettingStyle}"
|
||||
Width="120"/>
|
||||
IsOn="{Binding UnlockFps,Mode=TwoWay}"
|
||||
OnContent="Enable"
|
||||
OffContent="Disable"
|
||||
Style="{StaticResource ToggleSwitchSettingStyle}"
|
||||
Width="120"/>
|
||||
</sc:Setting.ActionContent>
|
||||
</sc:Setting>
|
||||
<sc:Setting
|
||||
Header="Set frame rate"
|
||||
Description="60">
|
||||
Description="{Binding TargetFps}">
|
||||
<sc:Setting.ActionContent>
|
||||
<Slider
|
||||
Value="{Binding TargetFps,Mode=TwoWay}"
|
||||
Minimum="60"
|
||||
Maximum="360"
|
||||
Width="400"/>
|
||||
@@ -143,6 +249,7 @@
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{StaticResource SystemControlAcrylicElementMediumHighBrush}">
|
||||
<Button
|
||||
Command="{Binding LaunchCommand}"
|
||||
HorizontalAlignment="Right"
|
||||
Grid.Column="3"
|
||||
Margin="24"
|
||||
@@ -150,4 +257,4 @@
|
||||
Content="启动游戏"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</control:ScopedPage>
|
||||
</shc:ScopedPage>
|
||||
@@ -63,7 +63,7 @@ public sealed partial class LoginMihoyoBBSPage : Microsoft.UI.Xaml.Controls.Page
|
||||
infoBarService.Information($"此 Cookie 不完整,操作失败");
|
||||
break;
|
||||
case UserOptionResult.Invalid:
|
||||
infoBarService.Information($"此 Cookie 无法,操作失败");
|
||||
infoBarService.Information($"此 Cookie 无效,操作失败");
|
||||
break;
|
||||
case UserOptionResult.Updated:
|
||||
infoBarService.Success($"用户 [{nickname}] 更新成功");
|
||||
|
||||
@@ -28,7 +28,6 @@ public sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.Pag
|
||||
private async void OnRootLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await WebView.EnsureCoreWebView2Async();
|
||||
WebView.CoreWebView2.SourceChanged += OnCoreWebView2SourceChanged;
|
||||
|
||||
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
|
||||
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
|
||||
@@ -40,23 +39,10 @@ public sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.Pag
|
||||
WebView.CoreWebView2.Navigate("https://user.mihoyo.com/#/login/password");
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD100")]
|
||||
private async void OnCoreWebView2SourceChanged(CoreWebView2 sender, CoreWebView2SourceChangedEventArgs args)
|
||||
{
|
||||
if (sender != null)
|
||||
{
|
||||
if (sender.Source.ToString() == "https://user.mihoyo.com/#/account/home")
|
||||
{
|
||||
await HandleCurrentCookieAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleCurrentCookieAsync()
|
||||
{
|
||||
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
|
||||
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
|
||||
WebView.CoreWebView2.SourceChanged -= OnCoreWebView2SourceChanged;
|
||||
|
||||
Cookie cookie = Cookie.FromCoreWebView2Cookies(cookies);
|
||||
IUserService userService = Ioc.Default.GetRequiredService<IUserService>();
|
||||
|
||||
@@ -56,10 +56,10 @@
|
||||
<sc:Setting
|
||||
Icon=""
|
||||
Header="反馈"
|
||||
Description="只处理在 Github 上反馈的问题">
|
||||
Description="Github 上反馈的问题会优先处理">
|
||||
<HyperlinkButton
|
||||
Content="前往反馈"
|
||||
NavigateUri="http://go.hut.ao/issue"/>
|
||||
NavigateUri="https://hut.ao/statements/bug-report.html"/>
|
||||
</sc:Setting>
|
||||
<sc:SettingExpander>
|
||||
<sc:SettingExpander.Header>
|
||||
@@ -75,7 +75,11 @@
|
||||
IsOpen="True"
|
||||
CornerRadius="0,0,4,4">
|
||||
<InfoBar.ActionButton>
|
||||
<Button HorizontalAlignment="Right" Width="1" Content="没用的按钮"/>
|
||||
<Button
|
||||
HorizontalAlignment="Right"
|
||||
Width="1"
|
||||
Command="{Binding DebugExceptionCommand}"
|
||||
Content="没用的按钮"/>
|
||||
</InfoBar.ActionButton>
|
||||
</InfoBar>
|
||||
</sc:SettingExpander>
|
||||
@@ -105,7 +109,7 @@
|
||||
</sc:Setting.ActionContent>
|
||||
</sc:Setting>
|
||||
</sc:SettingsGroup>
|
||||
|
||||
|
||||
<sc:SettingsGroup Header="测试功能">
|
||||
<sc:Setting
|
||||
Icon=""
|
||||
@@ -134,7 +138,6 @@
|
||||
</sc:Setting.ActionContent>
|
||||
</sc:Setting>
|
||||
</sc:SettingsGroup>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
Background="Transparent"
|
||||
BorderBrush="{x:Null}"
|
||||
Grid.Column="2"
|
||||
Margin="4">
|
||||
Margin="4,4,4,6">
|
||||
<Button.Resources>
|
||||
<shc:BindingProxy
|
||||
x:Key="ViewModelBindingProxy"
|
||||
@@ -210,8 +210,8 @@
|
||||
<AppBarButton Label="网页登录" Icon="{shcm:FontIcon Glyph=}">
|
||||
<AppBarButton.Flyout>
|
||||
<MenuFlyout>
|
||||
<MenuFlyoutItem Icon="{shcm:FontIcon Glyph=}" Text="登录米游社原神社区" Command="{Binding LoginMihoyoBBSCommand}"/>
|
||||
<MenuFlyoutItem Icon="{shcm:FontIcon Glyph=}" Text="登录米哈游通行证" Command="{Binding LoginMihoyoUserCommand}"/>
|
||||
<MenuFlyoutItem Icon="{shcm:FontIcon Glyph=}" Text="添加 Cookie" Command="{Binding LoginMihoyoBBSCommand}"/>
|
||||
<MenuFlyoutItem Icon="{shcm:FontIcon Glyph=}" Text="升级 Stoken" Command="{Binding LoginMihoyoUserCommand}"/>
|
||||
</MenuFlyout>
|
||||
</AppBarButton.Flyout>
|
||||
</AppBarButton>
|
||||
|
||||
@@ -272,7 +272,7 @@ internal class AchievementViewModel
|
||||
[ThreadAccess(ThreadAccessState.MainThread)]
|
||||
private async Task OpenUIAsync()
|
||||
{
|
||||
bool metaInitialized = await metadataService.InitializeAsync(CancellationToken).ConfigureAwait(false);
|
||||
bool metaInitialized = await metadataService.InitializeAsync().ConfigureAwait(false);
|
||||
|
||||
if (metaInitialized)
|
||||
{
|
||||
|
||||
@@ -83,7 +83,7 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
|
||||
{
|
||||
if (user.SelectedUserGameRole is UserGameRole role)
|
||||
{
|
||||
return RefreshCoreAsync((PlayerUid)role, RefreshOption.DatabaseOnly);
|
||||
return RefreshCoreAsync((PlayerUid)role, RefreshOption.DatabaseOnly, CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
|
||||
{
|
||||
if (user.SelectedUserGameRole is UserGameRole role)
|
||||
{
|
||||
return RefreshCoreAsync((PlayerUid)role, RefreshOption.Standard);
|
||||
return RefreshCoreAsync((PlayerUid)role, RefreshOption.Standard, CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,31 +110,37 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
await RefreshCoreAsync(uid, RefreshOption.RequestFromAPI).ConfigureAwait(false);
|
||||
await RefreshCoreAsync(uid, RefreshOption.RequestFromAPI, CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshCoreAsync(PlayerUid uid, RefreshOption option)
|
||||
private async Task RefreshCoreAsync(PlayerUid uid, RefreshOption option, CancellationToken token)
|
||||
{
|
||||
(RefreshResult result, Summary? summary) = await avatarInfoService.GetSummaryAsync(uid, option).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
(RefreshResult result, Summary? summary) = await avatarInfoService.GetSummaryAsync(uid, option, token).ConfigureAwait(false);
|
||||
|
||||
if (result == RefreshResult.Ok)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
Summary = summary;
|
||||
SelectedAvatar = Summary?.Avatars.FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (result)
|
||||
if (result == RefreshResult.Ok)
|
||||
{
|
||||
case RefreshResult.APIUnavailable:
|
||||
infoBarService.Warning("角色信息服务当前不可用");
|
||||
break;
|
||||
case RefreshResult.ShowcaseNotOpen:
|
||||
infoBarService.Warning("角色橱窗尚未开启,请前往游戏操作后重试");
|
||||
break;
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
Summary = summary;
|
||||
SelectedAvatar = Summary?.Avatars.FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case RefreshResult.APIUnavailable:
|
||||
infoBarService.Warning("角色信息服务当前不可用");
|
||||
break;
|
||||
case RefreshResult.ShowcaseNotOpen:
|
||||
infoBarService.Warning("角色橱窗尚未开启,请前往游戏操作后重试");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,15 +66,20 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
|
||||
{
|
||||
HomaClient homaClient = Ioc.Default.GetRequiredService<HomaClient>();
|
||||
IUserService userService = Ioc.Default.GetRequiredService<IUserService>();
|
||||
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
|
||||
if (userService.Current is Model.Binding.User user)
|
||||
{
|
||||
if (user.SelectedUserGameRole == null)
|
||||
{
|
||||
infoBarService.Warning("尚未选择角色");
|
||||
}
|
||||
|
||||
SimpleRecord record = await homaClient.GetPlayerRecordAsync(user).ConfigureAwait(false);
|
||||
Web.Response.Response<string>? response = await homaClient.UploadRecordAsync(record).ConfigureAwait(false);
|
||||
|
||||
if (response != null && response.IsOk())
|
||||
{
|
||||
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
infoBarService.Success(response.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
private GachaStatistics? statistics;
|
||||
private bool isAggressiveRefresh;
|
||||
private HistoryWish? selectedHistoryWish;
|
||||
private bool isInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的祈愿记录视图模型
|
||||
@@ -112,6 +113,11 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
/// </summary>
|
||||
public bool IsAggressiveRefresh { get => isAggressiveRefresh; set => SetProperty(ref isAggressiveRefresh, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 初始化是否完成
|
||||
/// </summary>
|
||||
public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 页面加载命令
|
||||
/// </summary>
|
||||
@@ -183,6 +189,9 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
|
||||
{
|
||||
infoBarService.Information("请刷新或导入祈愿记录");
|
||||
}
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
IsInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Context.Database;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.LifeCycle;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Model.Binding.LaunchGame;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Game;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.ViewModel;
|
||||
|
||||
@@ -17,7 +27,11 @@ namespace Snap.Hutao.ViewModel;
|
||||
[Injection(InjectAs.Scoped)]
|
||||
internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
|
||||
{
|
||||
private static readonly string TrueString = true.ToString();
|
||||
private static readonly string FalseString = false.ToString();
|
||||
|
||||
private readonly IGameService gameService;
|
||||
private readonly AppDbContext appDbContext;
|
||||
|
||||
private readonly List<LaunchScheme> knownSchemes = new()
|
||||
{
|
||||
@@ -41,16 +55,19 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
|
||||
/// 构造一个新的启动游戏视图模型
|
||||
/// </summary>
|
||||
/// <param name="gameService">游戏服务</param>
|
||||
/// <param name="appDbContext">数据库上下文</param>
|
||||
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
|
||||
public LaunchGameViewModel(IGameService gameService, IAsyncRelayCommandFactory asyncRelayCommandFactory)
|
||||
public LaunchGameViewModel(IGameService gameService, AppDbContext appDbContext, IAsyncRelayCommandFactory asyncRelayCommandFactory)
|
||||
{
|
||||
this.gameService = gameService;
|
||||
this.appDbContext = appDbContext;
|
||||
|
||||
OpenUICommand = asyncRelayCommandFactory.Create(OpenUIAsync);
|
||||
LaunchCommand = asyncRelayCommandFactory.Create(LaunchAsync);
|
||||
DetectGameAccountCommand = asyncRelayCommandFactory.Create(DetectGameAccountAsync);
|
||||
ModifyGameAccountCommand = asyncRelayCommandFactory.Create(ModifyGameAccountAsync);
|
||||
RemoveGameAccountCommand = asyncRelayCommandFactory.Create(RemoveGameAccountAsync);
|
||||
ModifyGameAccountCommand = asyncRelayCommandFactory.Create<GameAccount>(ModifyGameAccountAsync);
|
||||
RemoveGameAccountCommand = asyncRelayCommandFactory.Create<GameAccount>(RemoveGameAccountAsync);
|
||||
AttachGameAccountCommand = new RelayCommand<GameAccount>(AttachGameAccountToCurrentUserGameRole);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -106,6 +123,12 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
|
||||
/// </summary>
|
||||
public int TargetFps { get => targetFps; set => SetProperty(ref targetFps, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否提权
|
||||
/// </summary>
|
||||
[SuppressMessage("Performance", "CA1822")]
|
||||
public bool IsElevated { get => Activation.GetElevated(); }
|
||||
|
||||
/// <summary>
|
||||
/// 打开界面命令
|
||||
/// </summary>
|
||||
@@ -131,34 +154,137 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
|
||||
/// </summary>
|
||||
public ICommand RemoveGameAccountCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 绑定到Uid命令
|
||||
/// </summary>
|
||||
public ICommand AttachGameAccountCommand { get; }
|
||||
|
||||
private async Task OpenUIAsync()
|
||||
{
|
||||
(bool isOk, string gamePath) = await gameService.GetGamePathAsync().ConfigureAwait(false);
|
||||
bool gameExists = File.Exists(gameService.GetGamePathSkipLocator());
|
||||
|
||||
if (isOk)
|
||||
if (gameExists)
|
||||
{
|
||||
MultiChannel multi = gameService.GetMultiChannel();
|
||||
SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel);
|
||||
GameAccounts = gameService.GetGameAccountCollection();
|
||||
|
||||
// Sync from Settings
|
||||
RetiveSetting();
|
||||
}
|
||||
else
|
||||
{
|
||||
Ioc.Default.GetRequiredService<IInfoBarService>().Warning("游戏路径不正确,前往设置更改游戏路径。");
|
||||
await Ioc.Default.GetRequiredService<INavigationService>().NavigateAsync<View.Page.SettingPage>(INavigationAwaiter.Default, true).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void RetiveSetting()
|
||||
{
|
||||
DbSet<SettingEntry> settings = appDbContext.Settings;
|
||||
|
||||
isFullScreen = settings.SingleOrAdd(SettingEntry.LaunchIsFullScreen, TrueString).GetBoolean();
|
||||
OnPropertyChanged(nameof(IsFullScreen));
|
||||
|
||||
isBorderless = settings.SingleOrAdd(SettingEntry.LaunchIsBorderless, FalseString).GetBoolean();
|
||||
OnPropertyChanged(nameof(IsBorderless));
|
||||
|
||||
screenWidth = settings.SingleOrAdd(SettingEntry.LaunchScreenWidth, "1920").GetInt32();
|
||||
OnPropertyChanged(nameof(ScreenWidth));
|
||||
|
||||
screenHeight = settings.SingleOrAdd(SettingEntry.LaunchScreenHeight, "1080").GetInt32();
|
||||
OnPropertyChanged(nameof(ScreenHeight));
|
||||
|
||||
unlockFps = settings.SingleOrAdd(SettingEntry.LaunchUnlockFps, FalseString).GetBoolean();
|
||||
OnPropertyChanged(nameof(UnlockFps));
|
||||
|
||||
targetFps = settings.SingleOrAdd(SettingEntry.LaunchTargetFps, "60").GetInt32();
|
||||
OnPropertyChanged(nameof(TargetFps));
|
||||
}
|
||||
|
||||
private void SaveSetting()
|
||||
{
|
||||
DbSet<SettingEntry> settings = appDbContext.Settings;
|
||||
settings.SingleOrAdd(SettingEntry.LaunchIsFullScreen, TrueString).SetBoolean(IsFullScreen);
|
||||
settings.SingleOrAdd(SettingEntry.LaunchIsBorderless, FalseString).SetBoolean(IsBorderless);
|
||||
settings.SingleOrAdd(SettingEntry.LaunchScreenWidth, "1920").SetInt32(ScreenWidth);
|
||||
settings.SingleOrAdd(SettingEntry.LaunchScreenHeight, "1080").SetInt32(ScreenHeight);
|
||||
settings.SingleOrAdd(SettingEntry.LaunchUnlockFps, FalseString).SetBoolean(UnlockFps);
|
||||
settings.SingleOrAdd(SettingEntry.LaunchTargetFps, "60").SetInt32(TargetFps);
|
||||
appDbContext.SaveChanges();
|
||||
}
|
||||
|
||||
private async Task LaunchAsync()
|
||||
{
|
||||
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
|
||||
if (gameService.IsGameRunning())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (SelectedScheme != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
gameService.SetMultiChannel(SelectedScheme);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
infoBarService.Warning("切换服务器失败,保存配置文件时发生异常\n请以管理员模式启动胡桃。");
|
||||
}
|
||||
}
|
||||
|
||||
if (SelectedGameAccount != null)
|
||||
{
|
||||
if (!gameService.SetGameAccount(SelectedGameAccount))
|
||||
{
|
||||
Ioc.Default.GetRequiredService<IInfoBarService>().Warning("切换账号失败");
|
||||
}
|
||||
}
|
||||
|
||||
SaveSetting();
|
||||
|
||||
LaunchConfiguration configuration = new(IsFullScreen, IsBorderless, ScreenWidth, ScreenHeight, IsElevated && UnlockFps, TargetFps);
|
||||
await gameService.LaunchAsync(configuration).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task DetectGameAccountAsync()
|
||||
{
|
||||
|
||||
await gameService.DetectGameAccountAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ModifyGameAccountAsync()
|
||||
private void AttachGameAccountToCurrentUserGameRole(GameAccount? gameAccount)
|
||||
{
|
||||
if (gameAccount != null)
|
||||
{
|
||||
IUserService userService = Ioc.Default.GetRequiredService<IUserService>();
|
||||
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
|
||||
if (userService.Current?.SelectedUserGameRole is UserGameRole role)
|
||||
{
|
||||
gameService.AttachGameAccountToUid(gameAccount, role.GameUid);
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning("当前未选择角色");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveGameAccountAsync()
|
||||
private async Task ModifyGameAccountAsync(GameAccount? gameAccount)
|
||||
{
|
||||
if (gameAccount != null)
|
||||
{
|
||||
await gameService.ModifyGameAccountAsync(gameAccount).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveGameAccountAsync(GameAccount? gameAccount)
|
||||
{
|
||||
if (gameAccount != null)
|
||||
{
|
||||
await gameService.RemoveGameAccountAsync(gameAccount).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Snap.Hutao.Context.Database;
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
@@ -46,11 +47,13 @@ internal class SettingViewModel : ObservableObject
|
||||
GamePath = gameService.GetGamePathSkipLocator();
|
||||
|
||||
SetGamePathCommand = asyncRelayCommandFactory.Create(SetGamePathAsync);
|
||||
DebugExceptionCommand = new RelayCommand(DebugThrowException);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 版本
|
||||
/// </summary>
|
||||
[SuppressMessage("", "CA1822")]
|
||||
public string AppVersion
|
||||
{
|
||||
get => Core.CoreEnvironment.Version.ToString();
|
||||
@@ -90,6 +93,11 @@ internal class SettingViewModel : ObservableObject
|
||||
/// </summary>
|
||||
public ICommand SetGamePathCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 调试异常命令
|
||||
/// </summary>
|
||||
public ICommand DebugExceptionCommand { get; }
|
||||
|
||||
private async Task SetGamePathAsync()
|
||||
{
|
||||
IGameLocator locator = Ioc.Default.GetRequiredService<IEnumerable<IGameLocator>>()
|
||||
@@ -103,4 +111,11 @@ internal class SettingViewModel : ObservableObject
|
||||
GamePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
private void DebugThrowException()
|
||||
{
|
||||
#if DEBUG
|
||||
throw new InvalidOperationException("测试用异常");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ internal class UserViewModel : ObservableObject
|
||||
infoBarService.Information($"此 Cookie 不完整,操作失败");
|
||||
break;
|
||||
case UserOptionResult.Invalid:
|
||||
infoBarService.Information($"此 Cookie 无法,操作失败");
|
||||
infoBarService.Information($"此 Cookie 无效,操作失败");
|
||||
break;
|
||||
case UserOptionResult.Updated:
|
||||
infoBarService.Success($"用户 [{uid}] 更新成功");
|
||||
@@ -162,7 +162,7 @@ internal class UserViewModel : ObservableObject
|
||||
Verify.Operation(user != null, "待复制 Cookie 的用户不应为 null");
|
||||
try
|
||||
{
|
||||
Clipboard.SetText(user.Cookie.ToString());
|
||||
Clipboard.SetText(user.Cookie!.ToString());
|
||||
infoBarService.Success($"{user.UserInfo!.Nickname} 的 Cookie 复制成功");
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
@@ -161,6 +161,8 @@ internal class WikiAvatarViewModel : ObservableObject
|
||||
await CombineWithAvatarCollocationsAsync(sorted).ConfigureAwait(false);
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
|
||||
// RPC_E_WRONG_THREAD ?
|
||||
Avatars = new AdvancedCollectionView(sorted, true);
|
||||
Selected = Avatars.Cast<Avatar>().FirstOrDefault();
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ internal static class HttpClientExtensions
|
||||
/// <returns>客户端</returns>
|
||||
internal static HttpClient SetUser(this HttpClient httpClient, User user)
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie.ToString());
|
||||
httpClient.DefaultRequestHeaders.Set("Cookie", user.Cookie!.ToString());
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ namespace Snap.Hutao.Web.Hoyolab;
|
||||
/// </summary>
|
||||
public struct PlayerUid
|
||||
{
|
||||
[SuppressMessage("", "CA1805")]
|
||||
[SuppressMessage("", "IDE0079")]
|
||||
private string? region = default;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -202,4 +202,4 @@ internal class HomaClient
|
||||
{
|
||||
return httpClient.TryCatchPostAsJsonAsync<SimpleRecord, Response<string>>($"{HutaoAPI}/Record/Upload", playerRecord, options, logger, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
52
src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaClient2.cs
Normal file
52
src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaClient2.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hutao.Log;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Web.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃日志客户端
|
||||
/// </summary>
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class HomaClient2
|
||||
{
|
||||
private const string HutaoAPI = "https://homa.snapgenshin.com";
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的胡桃日志客户端
|
||||
/// </summary>
|
||||
/// <param name="httpClient">Http客户端</param>
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
public HomaClient2(HttpClient httpClient, JsonSerializerOptions options)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传日志
|
||||
/// </summary>
|
||||
/// <param name="exception">异常</param>
|
||||
/// <returns>任务</returns>
|
||||
public async Task<string?> UploadLogAsync(Exception exception)
|
||||
{
|
||||
HutaoLog log = new()
|
||||
{
|
||||
Id = Core.CoreEnvironment.HutaoDeviceId,
|
||||
Time = DateTimeOffset.Now.ToUnixTimeMilliseconds(),
|
||||
Info = exception.ToString(),
|
||||
};
|
||||
|
||||
Response<string>? a = await httpClient
|
||||
.TryCatchPostAsJsonAsync<HutaoLog, Response<string>>($"{HutaoAPI}/HutaoLog/Upload", log, options)
|
||||
.ConfigureAwait(false);
|
||||
return a?.Data;
|
||||
}
|
||||
}
|
||||
25
src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HutaoLog.cs
Normal file
25
src/Snap.Hutao/Snap.Hutao/Web/Hutao/Log/HutaoLog.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hutao.Log;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃日志
|
||||
/// </summary>
|
||||
public class HutaoLog
|
||||
{
|
||||
/// <summary>
|
||||
/// 设备Id
|
||||
/// </summary>
|
||||
public string Id { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 崩溃时间
|
||||
/// </summary>
|
||||
public long Time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误信息
|
||||
/// </summary>
|
||||
public string Info { get; set; } = default!;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ public class SimpleRank
|
||||
/// 构造一个新的数值
|
||||
/// </summary>
|
||||
/// <param name="rank">排行</param>
|
||||
public SimpleRank(Rank rank)
|
||||
private SimpleRank(Rank rank)
|
||||
{
|
||||
AvatarId = rank.AvatarId;
|
||||
Value = rank.Value;
|
||||
@@ -29,4 +29,19 @@ public class SimpleRank
|
||||
/// 值
|
||||
/// </summary>
|
||||
public int Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的简单数值
|
||||
/// </summary>
|
||||
/// <param name="rank">排行</param>
|
||||
/// <returns>新的简单数值</returns>
|
||||
public static SimpleRank? FromRank(Rank? rank)
|
||||
{
|
||||
if (rank == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SimpleRank(rank);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ public class SimpleSpiralAbyss
|
||||
public SimpleSpiralAbyss(SpiralAbyss spiralAbyss)
|
||||
{
|
||||
ScheduleId = spiralAbyss.ScheduleId;
|
||||
Damage = new(spiralAbyss.DamageRank.Single());
|
||||
TakeDamage = new(spiralAbyss.TakeDamageRank.Single());
|
||||
Damage = SimpleRank.FromRank(spiralAbyss.DamageRank.SingleOrDefault());
|
||||
TakeDamage = SimpleRank.FromRank(spiralAbyss.TakeDamageRank.SingleOrDefault());
|
||||
Floors = spiralAbyss.Floors.Select(f => new SimpleFloor(f));
|
||||
}
|
||||
|
||||
@@ -30,12 +30,12 @@ public class SimpleSpiralAbyss
|
||||
/// <summary>
|
||||
/// 造成伤害
|
||||
/// </summary>
|
||||
public SimpleRank Damage { get; set; } = default!;
|
||||
public SimpleRank? Damage { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 受到伤害
|
||||
/// </summary>
|
||||
public SimpleRank TakeDamage { get; set; } = default!;
|
||||
public SimpleRank? TakeDamage { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 层
|
||||
|
||||
Reference in New Issue
Block a user