Compare commits

..

4 Commits

Author SHA1 Message Date
DismissedLight
77db918178 update bug report template 2022-11-05 21:35:57 +08:00
DismissedLight
77158fc708 add installer 2022-11-05 20:45:38 +08:00
DismissedLight
f2d63e69ea bug fixes 2022-11-04 17:45:17 +08:00
DismissedLight
9e344f56e0 LaunchGame 2022-11-03 18:19:36 +08:00
119 changed files with 2281 additions and 1196 deletions

View File

@@ -48,9 +48,9 @@ body:
- type: textarea
id: logs
attributes:
label: 相关的崩溃日志 位于 `%HOMEPATH%/Documents/Hutao/Log.db`
label: 相关的崩溃日志
description: |
在资源管理器中直接输入`%HOMEPATH%/Documents/Hutao`即可进入文件夹
在资源管理器中直接输入`%userprofile%/Documents/Hutao`即可进入文件夹
如果应用程序崩溃了,请将`log.db` 文件上传,文件包含了敏感信息,谨慎上传
如果这个表单是关于导入祈愿记录的问题,请包含你导入的`Json`文件
**务必不要上传`user.db`文件,该文件包含你的帐号敏感信息**

4
.gitignore vendored
View File

@@ -11,6 +11,10 @@ src/Snap.Hutao/Snap.Hutao/bin/
src/Snap.Hutao/Snap.Hutao/obj/
src/Snap.Hutao/Snap.Hutao/Snap.Hutao_TemporaryKey.pfx
src/Snap.Hutao/Snap.Hutao.Installer/bin/
src/Snap.Hutao/Snap.Hutao.Installer/obj/
src/Snap.Hutao/Snap.Hutao.Installer/Properties/PublishProfiles/FolderProfile.pubxml.user
src/Snap.Hutao/Snap.Hutao.SourceGeneration/bin/
src/Snap.Hutao/Snap.Hutao.SourceGeneration/obj/

View File

@@ -36,5 +36,4 @@
* [microsoft/vs-threading](https://github.com/microsoft/vs-threading)
* [microsoft/vs-validation](https://github.com/microsoft/vs-validation)
* [microsoft/WindowsAppSDK](https://github.com/microsoft/WindowsAppSDK)
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)
* [MiniExcel/MiniExcel](https://github.com/MiniExcel/MiniExcel)
* [microsoft/microsoft-ui-xaml](https://github.com/microsoft/microsoft-ui-xaml)

View File

@@ -0,0 +1,72 @@
using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace Snap.Hutao.Installer;
internal class Program
{
private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Appx";
private const string ValueName = "AllowDevelopmentWithoutDevLicense";
public static async Task Main(string[] args)
{
string ps1File = Path.Combine(AppContext.BaseDirectory, "Install.ps1");
if (!File.Exists(ps1File))
{
Console.WriteLine("未检测到 Install.ps1 文件");
Console.WriteLine("请勿移动该安装程序,按下任意键退出...");
Console.ReadKey();
return;
}
try
{
//以管理策略打开开发者模式
Registry.SetValue(AppxKey, ValueName, 1, RegistryValueKind.DWord);
}
catch (Exception)
{
Console.WriteLine("开发者模式未开启,请手动开启,参阅下方链接");
Console.WriteLine("https://learn.microsoft.com/zh-CN/windows/apps/get-started/developer-mode-features-and-debugging");
}
await InstallAsync(ps1File).ConfigureAwait(false);
Console.WriteLine();
Console.WriteLine("官方文档与使用教程");
Console.WriteLine("https://hut.ao");
Console.WriteLine();
Console.WriteLine("在开始菜单中启动 Snap.Hutao ,按下任意键退出...");
Console.ReadKey();
}
private static async Task InstallAsync(string ps1File)
{
Console.WriteLine("请注意 PowerShell 中的提示");
Process ps = new()
{
StartInfo = new ProcessStartInfo()
{
FileName = "powershell.exe",
Arguments = $"-ExecutionPolicy Unrestricted \"{ps1File}\"",
UseShellExecute = true,
}
};
try
{
ps.Start();
await ps.WaitForExitAsync();
Console.WriteLine("安装脚本运行完成");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishTrimmed>true</PublishTrimmed>
<ApplicationManifest>app.manifest</ApplicationManifest>
<DebugType>embedded</DebugType>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- UAC 清单选项
如果想要更改 Windows 用户帐户控制级别,请使用
以下节点之一替换 requestedExecutionLevel 节点。
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
如果你的应用程序需要此虚拟化来实现向后兼容性,则移除此
元素。
-->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的
Windows 版本的列表。取消评论适当的元素,
Windows 将自动选择最兼容的环境。 -->
<!-- Windows Vista -->
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
<!-- Windows 7 -->
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
<!-- Windows 8 -->
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
<!-- Windows 8.1 -->
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<!-- 指示该应用程序可感知 DPI 且 Windows 在 DPI 较高时将不会对其进行
自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI无需
选择加入。选择加入此设置的 Windows 窗体应用程序(面向 .NET Framework 4.6)还应
在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。
将应用程序设为感知长路径。请参阅 https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
<!--
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
-->
<!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) -->
<!--
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
-->
</assembly>

View File

@@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SettingsUI", "SettingsUI\Se
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{8B96721E-5604-47D2-9B72-06FEBAD0CE00}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snap.Hutao.Installer", "Snap.Hutao.Installer\Snap.Hutao.Installer.csproj", "{CEC01691-F65E-4874-9AE2-F571369A7631}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -82,6 +84,22 @@ Global
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|arm64.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|arm64.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x64.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x64.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x86.ActiveCfg = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Debug|x86.Build.0 = Debug|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|Any CPU.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|arm64.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|arm64.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x64.Build.0 = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x86.ActiveCfg = Release|Any CPU
{CEC01691-F65E-4874-9AE2-F571369A7631}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -25,6 +25,8 @@
<Thickness x:Key="InfoBarContentRootPadding">16,0,0,0</Thickness>
<!--Pivot Resource-->
<x:Double x:Key="PivotHeaderItemFontSize">16</x:Double>
<Thickness x:Key="PivotHeaderItemMargin">16,0,0,0</Thickness>
<Thickness x:Key="PivotItemMargin">0</Thickness>
<!--CornerRadius-->
<CornerRadius x:Key="CompatCornerRadius">6</CornerRadius>
<CornerRadius x:Key="CompatCornerRadiusTop">6,6,0,0</CornerRadius>
@@ -46,6 +48,22 @@
<shmmc:QualityColorConverter x:Key="QualityColorConverter"/>
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
<!--Styles-->
<Style
x:Key="LargeGridViewItemStyle"
TargetType="GridViewItem"
BasedOn="{StaticResource DefaultGridViewItemStyle}">
<Setter Property="Margin" Value="0,0,12,12"/>
</Style>
<!--ItemsPanelTemplate-->
<ItemsPanelTemplate x:Key="ItemsStackPanelTemplate">
<ItemsStackPanel/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="HorizontalStackPanelTemplate">
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -7,9 +7,7 @@ using Snap.Hutao.Core;
using Snap.Hutao.Core.Exception;
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 +27,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 +59,6 @@ public partial class App : Application
.ImplictAs<IMetadataInitializer>()?
.InitializeInternalAsync()
.SafeForget(logger);
Ioc.Default
.GetRequiredService<AppCenter>()
.Initialize();
}
else
{

View File

@@ -30,6 +30,7 @@ public class ScopedPage : Page
/// <summary>
/// 初始化
/// 应当在 InitializeComponent() 前调用
/// </summary>
/// <typeparam name="TViewModel">视图模型类型</typeparam>
public void InitializeWith<TViewModel>()

View File

@@ -18,5 +18,5 @@ internal interface ISupportAsyncInitialization
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>初始化任务</returns>
ValueTask<bool> InitializeAsync(CancellationToken token = default);
ValueTask<bool> InitializeAsync();
}

View File

@@ -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()

View File

@@ -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;

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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>>())

View File

@@ -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());
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -28,4 +28,4 @@ internal class ConcurrentCancellationTokenSource<TItem>
return waitingItems.GetOrAdd(item, new CancellationTokenSource()).Token;
}
}
}

View File

@@ -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>

View File

@@ -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();
}
@@ -154,5 +160,9 @@ internal sealed class ExtendedWindow
// 48 is the navigation button leftInset
RectInt32 dragRect = new RectInt32(48, 0, (int)titleBar.ActualWidth, (int)titleBar.ActualHeight).Scale(scale);
appTitleBar.SetDragRectangles(dragRect.Enumerate().ToArray());
// workaround for https://github.com/microsoft/WindowsAppSDK/issues/2976
// add this to set the same window size after every time drag rectangles are set
appWindow.ResizeClient(appWindow.ClientSize);
}
}
}

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -101,7 +101,7 @@ public class SystemBackdrop
private class DispatcherQueueHelper
{
private object? dispatcherQueueController = null;
private object? dispatcherQueueController;
/// <summary>
/// 确保系统调度队列控制器存在

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -11,6 +11,7 @@ global using Microsoft.Extensions.Logging;
// Snap.Hutao
global using Snap.Hutao.Core.DependencyInjection;
global using Snap.Hutao.Core.DependencyInjection.Annotation;
global using Snap.Hutao.Core.Threading;
global using Snap.Hutao.Core.Validation;
// Runtime

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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")

View File

@@ -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>

View File

@@ -21,6 +21,7 @@ public class ComplexAvatar
{
Name = avatar.Name;
Icon = AvatarIconConverter.IconNameToUri(avatar.Icon);
SideIcon = AvatarSideIconConverter.IconNameToUri(avatar.SideIcon);
Quality = avatar.Quality;
Rate = $"{rate:P3}";
}
@@ -35,6 +36,11 @@ public class ComplexAvatar
/// </summary>
public Uri Icon { get; set; } = default!;
/// <summary>
/// 侧面图标
/// </summary>
public Uri SideIcon { get; set; } = default!;
/// <summary>
/// 星级
/// </summary>

View File

@@ -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)

View File

@@ -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)));
}
}

View File

@@ -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>

View File

@@ -29,7 +29,7 @@ public class User : ISelectable
/// <summary>
/// 用户的Cookie
/// </summary>
public Cookie Cookie { get; set; } = default!;
public Cookie? Cookie { get; set; }
/// <summary>
/// 创建一个新的用户

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -12,9 +12,19 @@ internal class AvatarSideIconConverter : ValueConverterBase<string, Uri>
{
private const string BaseUrl = "https://static.snapgenshin.com/AvatarIcon/{0}.png";
/// <summary>
/// 名称转Uri
/// </summary>
/// <param name="name">名称</param>
/// <returns>链接</returns>
public static Uri IconNameToUri(string name)
{
return new Uri(string.Format(BaseUrl, name));
}
/// <inheritdoc/>
public override Uri Convert(string from)
{
return new Uri(string.Format(BaseUrl, from));
return IconNameToUri(from);
}
}

View File

@@ -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.21.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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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; }
}

View File

@@ -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;
}

View File

@@ -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; }
}

View File

@@ -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"; }
}

View File

@@ -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!;
}

View File

@@ -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; }
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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,
}

View File

@@ -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"; }
}

View File

@@ -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"; }
}

View File

@@ -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>();
}

View File

@@ -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"; }
}

View File

@@ -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"; }
}

View File

@@ -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; }
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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))

View File

@@ -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;
}

View File

@@ -58,7 +58,7 @@ internal interface IGachaLogService
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>是否初始化成功</returns>
ValueTask<bool> InitializeAsync(CancellationToken token = default);
ValueTask<bool> InitializeAsync();
/// <summary>
/// 刷新祈愿记录

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
@@ -36,7 +40,6 @@ internal class GameService : IGameService
/// </summary>
/// <param name="scopeFactory">范围工厂</param>
/// <param name="memoryCache">内存缓存</param>
/// <param name="gameLocators">游戏定位器集合</param>
public GameService(IServiceScopeFactory scopeFactory, IMemoryCache memoryCache)
{
this.scopeFactory = scopeFactory;
@@ -56,7 +59,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 +89,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 +144,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 +156,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 +219,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 +253,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.
}
}
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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 },
};
}

View File

@@ -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>
/// 异步获取成就列表

View File

@@ -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;

View File

@@ -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>

View File

@@ -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);

View File

@@ -48,8 +48,10 @@
<None Remove="Resource\Icon\UI_Icon_Locked.png" />
<None Remove="Resource\Icon\UI_Icon_None.png" />
<None Remove="Resource\Icon\UI_ItemIcon_201.png" />
<None Remove="Resource\Icon\UI_ItemIcon_210.png" />
<None Remove="Resource\Segoe Fluent Icons.ttf" />
<None Remove="stylecop.json" />
<None Remove="View\Control\BottomTextControl.xaml" />
<None Remove="View\Control\DescParamComboBox.xaml" />
<None Remove="View\Control\ItemIcon.xaml" />
<None Remove="View\Control\SkillPivot.xaml" />
@@ -60,13 +62,14 @@
<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" />
<None Remove="View\Page\AnnouncementContentPage.xaml" />
<None Remove="View\Page\AnnouncementPage.xaml" />
<None Remove="View\Page\AvatarPropertyPage.xaml" />
<None Remove="View\Page\DailyNotePage.xaml" />
<None Remove="View\Page\GachaLogPage.xaml" />
<None Remove="View\Page\HutaoDatabasePage.xaml" />
<None Remove="View\Page\LaunchGamePage.xaml" />
@@ -104,6 +107,7 @@
<Content Include="Resource\Icon\UI_Icon_Locked.png" />
<Content Include="Resource\Icon\UI_Icon_None.png" />
<Content Include="Resource\Icon\UI_ItemIcon_201.png" />
<Content Include="Resource\Icon\UI_ItemIcon_210.png" />
</ItemGroup>
<ItemGroup>
@@ -150,6 +154,21 @@
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\DailyNotePage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Control\BottomTextControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</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 +209,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>

View File

@@ -0,0 +1,26 @@
<ContentControl
x:Class="Snap.Hutao.View.Control.BottomTextControl"
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">
<Border
BorderThickness="1"
CornerRadius="{StaticResource CompatCornerRadius}"
BorderBrush="{StaticResource CardStrokeColorDefault}"
Background="{StaticResource CardBackgroundFillColorDefault}">
<StackPanel>
<ContentPresenter
Name="ContentHost"/>
<TextBlock
Name="TextHost"
Margin="0,0,0,2"
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis"
MaxWidth="80"
HorizontalAlignment="Center"/>
</StackPanel>
</Border>
</ContentControl>

View File

@@ -0,0 +1,55 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using Snap.Hutao.Core;
namespace Snap.Hutao.View.Control;
/// <summary>
/// 底部带有文本的控件
/// </summary>
[ContentProperty(Name = nameof(TopContent))]
public sealed partial class BottomTextControl : ContentControl
{
private static readonly DependencyProperty TextProperty = Property<BottomTextControl>.Depend(nameof(Text), string.Empty, OnTextChanged);
private static readonly DependencyProperty TopContentProperty = Property<BottomTextControl>.Depend<UIElement>(nameof(TopContent), default!, OnContentChanged2);
/// <summary>
/// 构造一个新的底部带有文本的控件
/// </summary>
public BottomTextControl()
{
InitializeComponent();
}
/// <summary>
/// 顶部内容
/// </summary>
public UIElement TopContent
{
get { return (UIElement)GetValue(TopContentProperty); }
set { SetValue(TopContentProperty, value); }
}
/// <summary>
/// 文本
/// </summary>
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
private static void OnTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs dp)
{
((BottomTextControl)sender).TextHost.Text = (string)dp.NewValue;
}
private static void OnContentChanged2(DependencyObject sender, DependencyPropertyChangedEventArgs dp)
{
((BottomTextControl)sender).ContentHost.Content = dp.NewValue;
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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;
}
}
}
}

View File

@@ -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="&#xEB41;"
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>

View File

@@ -38,6 +38,11 @@
shvh:NavHelper.NavigateTo="shvp:GachaLogPage"
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Icon/UI_BtnIcon_Gacha.png}"/>
<NavigationViewItem
Content="实时便笺"
shvh:NavHelper.NavigateTo="shvp:DailyNotePage"
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Icon/UI_ItemIcon_210.png}"/>
<NavigationViewItem
Content="成就管理"
shvh:NavHelper.NavigateTo="shvp:AchievementPage"

View File

@@ -18,22 +18,22 @@
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid>
<Grid.Background>
<SolidColorBrush Color="{ThemeResource CardBackgroundFillColorSecondary}"/>
</Grid.Background>
<Grid Background="{StaticResource CardBackgroundFillColorDefaultBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="252"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CommandBar
Grid.Column="1"
DefaultLabelPosition="Right">
<CommandBar.Content>
<AutoSuggestBox
Text="{Binding SearchText,Mode=TwoWay}"
@@ -57,7 +57,7 @@
<AppBarElementContainer>
<ComboBox
MinWidth="120"
MinWidth="140"
Height="36"
Margin="2,6,3,6"
DisplayMemberPath="Name"
@@ -138,16 +138,8 @@
<ScrollViewer Padding="0,0,16,0">
<ItemsControl
Margin="16,0,0,16"
ItemsSource="{Binding Achievements}">
<!--ContentThemeTransition here can make items blinking-->
<!--<ItemsControl.Transitions>
<ContentThemeTransition/>
</ItemsControl.Transitions>-->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
ItemsSource="{Binding Achievements}"
ItemsPanel="{StaticResource ItemsStackPanelTemplate}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid
@@ -194,8 +186,7 @@
VerticalAlignment="Center"
Grid.Column="0"
Text="{Binding Time}"
Visibility="{Binding IsChecked,Converter={StaticResource BoolToVisibilityConverter}}"
/>
Visibility="{Binding IsChecked,Converter={StaticResource BoolToVisibilityConverter}}"/>
<Image
Grid.Column="1"
Height="32"

View File

@@ -14,6 +14,8 @@
xmlns:shca="using:Snap.Hutao.Control.Animation"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shv="using:Snap.Hutao.ViewModel"
d:DataContext="{d:DesignInstance shv:AnnouncementViewModel}"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<mxi:Interaction.Behaviors>
@@ -28,7 +30,7 @@
HorizontalAlignment="Stretch"
ItemsSource="{Binding Announcement.List}"
Padding="0"
Margin="12,12,0,-12">
Margin="16,16,0,-6">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
@@ -41,13 +43,9 @@
SelectionMode="None"
DesiredWidth="300"
HorizontalAlignment="Stretch"
ItemsSource="{Binding List}"
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}"
ItemsSource="{Binding List}"
Margin="0,0,2,0">
<cwucont:AdaptiveGridView.ItemContainerStyle>
<Style TargetType="GridViewItem" BasedOn="{StaticResource DefaultGridViewItemStyle}">
<Setter Property="Margin" Value="0,0,12,12"/>
</Style>
</cwucont:AdaptiveGridView.ItemContainerStyle>
<cwucont:AdaptiveGridView.ItemTemplate>
<DataTemplate>
<Border

View File

@@ -15,9 +15,7 @@
xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shcp="using:Snap.Hutao.Control.Panel"
xmlns:shct="using:Snap.Hutao.Control.Text"
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
xmlns:shvcont="using:Snap.Hutao.View.Control"
xmlns:shvconv="using:Snap.Hutao.View.Converter"
xmlns:shv="using:Snap.Hutao.ViewModel"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
@@ -45,7 +43,7 @@
</Grid.RowDefinitions>
<CommandBar
DefaultLabelPosition="Right"
Background="{StaticResource CardBackgroundFillColorSecondary}">
Background="{StaticResource CardBackgroundFillColorSecondaryBrush}">
<CommandBar.Content>
<Grid>
<Grid.ColumnDefinitions>
@@ -54,7 +52,9 @@
<ColumnDefinition Width="72"/>
<ColumnDefinition Width="72"/>
</Grid.ColumnDefinitions>
<shcp:PanelSelector Margin="6,6,0,0" x:Name="ItemsPanelSelector"/>
<shcp:PanelSelector
Margin="6,6,0,0"
x:Name="ItemsPanelSelector"/>
<StackPanel
Grid.Column="1"
Margin="12,6,0,0">
@@ -90,12 +90,12 @@
</StackPanel>
</Grid>
</CommandBar.Content>
<AppBarSeparator/>
<AppBarButton
Label="刷新"
Icon="{shcm:FontIcon Glyph=&#xE72C;}"
Command="{Binding RefreshByUserGameRoleCommand}"/>
<AppBarButton
Label="按UID查询"
Icon="{shcm:FontIcon Glyph=&#xE721;}"
@@ -236,12 +236,8 @@
CornerRadius="{StaticResource CompatCornerRadius}"
Background="{StaticResource CardBackgroundFillColorSecondary}">
<ItemsControl
ItemsSource="{Binding SelectedAvatar.Skills}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
ItemsSource="{Binding SelectedAvatar.Skills}"
ItemsPanel="{StaticResource HorizontalStackPanelTemplate}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button
@@ -450,12 +446,8 @@
SelectionMode="None"
HorizontalAlignment="Left"
ItemsSource="{Binding SelectedAvatar.Reliquaries}"
Margin="0,12,0,-12">
<cwucont:AdaptiveGridView.ItemContainerStyle>
<Style TargetType="GridViewItem" BasedOn="{StaticResource DefaultGridViewItemStyle}">
<Setter Property="Margin" Value="0,0,12,12"/>
</Style>
</cwucont:AdaptiveGridView.ItemContainerStyle>
Margin="0,12,0,-12"
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border

View File

@@ -0,0 +1,53 @@
<shc:ScopedPage
x:Class="Snap.Hutao.View.Page.DailyNotePage"
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:shc="using:Snap.Hutao.Control"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:sc="using:SettingsUI.Controls"
xmlns:shv="using:Snap.Hutao.ViewModel"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance shv:DailyNoteViewModel}"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<CommandBar
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
DefaultLabelPosition="Right">
<AppBarButton Label="添加角色" Icon="{shcm:FontIcon Glyph=&#xE710;}"/>
<AppBarButton Label="立即刷新" Icon="{shcm:FontIcon Glyph=&#xE72C;}"/>
<AppBarButton Label="通知设置" Icon="{shcm:FontIcon Glyph=&#xE713;}">
<AppBarButton.Flyout>
<Flyout>
<StackPanel>
<RadioButtons ItemsSource="{Binding RefreshTimes}">
<RadioButtons.Header>
<TextBlock Text="刷新间隔时间" Style="{StaticResource BaseTextBlockStyle}"/>
</RadioButtons.Header>
<RadioButtons.ItemTemplate>
<DataTemplate>
<TextBlock Margin="0,0,0,0" Text="{Binding Name}"/>
</DataTemplate>
</RadioButtons.ItemTemplate>
</RadioButtons>
<sc:SettingsGroup Header="通知" Margin="0,-16,0,0">
<sc:Setting
Icon="&#xEA8F;"
Header="提醒通知"
Description="防止通知自动收入操作中心">
<ToggleSwitch Margin="24,0,0,0" Style="{StaticResource ToggleSwitchSettingStyle}"/>
</sc:Setting>
</sc:SettingsGroup>
</StackPanel>
</Flyout>
</AppBarButton.Flyout>
</AppBarButton>
</CommandBar>
</Grid>
</shc:ScopedPage>

View File

@@ -0,0 +1,22 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Control;
using Snap.Hutao.ViewModel;
namespace Snap.Hutao.View.Page;
/// <summary>
/// 实时便笺页面
/// </summary>
public sealed partial class DailyNotePage : ScopedPage
{
/// <summary>
/// 构造一个新的实时便笺页面
/// </summary>
public DailyNotePage()
{
InitializeWith<DailyNoteViewModel>();
InitializeComponent();
}
}

View File

@@ -19,12 +19,7 @@
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<shc:ScopedPage.Resources>
<Thickness x:Key="PivotHeaderItemMargin">8,0,8,0</Thickness>
<Thickness x:Key="PivotItemMargin">0</Thickness>
</shc:ScopedPage.Resources>
<Grid>
<Grid Visibility="{Binding IsInitialized,Converter={StaticResource BoolToVisibilityConverter}}">
<Rectangle
Height="48"
VerticalAlignment="Top"
@@ -34,7 +29,7 @@
<ComboBox
MinWidth="120"
Height="36"
Margin="16,6,12,6"
Margin="16,6,0,6"
DisplayMemberPath="Uid"
SelectedItem="{Binding SelectedArchive,Mode=TwoWay}"
ItemsSource="{Binding Archives}"/>
@@ -44,14 +39,14 @@
<AppBarButton Label="刷新" Icon="{shcm:FontIcon Glyph=&#xE72C;}">
<AppBarButton.Flyout>
<MenuFlyout Placement="Bottom">
<MenuFlyoutItem
Text="从缓存刷新"
Icon="{shcm:FontIcon Glyph=&#xE81E;}"
Command="{Binding RefreshByWebCacheCommand}"/>
<MenuFlyoutItem
Text="Stoken刷新"
Icon="{shcm:FontIcon Glyph=&#xE192;}"
Command="{Binding RefreshByStokenCommand}"/>
<MenuFlyoutItem
Text="从缓存刷新"
Icon="{shcm:FontIcon Glyph=&#xE81E;}"
Command="{Binding RefreshByWebCacheCommand}"/>
<MenuFlyoutItem
Text="手动输入Url"
Icon="{shcm:FontIcon Glyph=&#xE765;}"

View File

@@ -15,16 +15,11 @@
mc:Ignorable="d"
d:DataContext="{d:DesignInstance shv:HutaoDatabaseViewModel}"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<shc:ScopedPage.Resources>
<Thickness x:Key="PivotHeaderItemMargin">8,0,8,0</Thickness>
<Thickness x:Key="PivotItemMargin">0</Thickness>
</shc:ScopedPage.Resources>
<Grid>
<Pivot>
<Pivot.RightHeader>
@@ -55,25 +50,15 @@
<GridView
SelectionMode="None"
ItemsSource="{Binding Avatars}"
Margin="12,12,0,-12">
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem" BasedOn="{StaticResource DefaultGridViewItemStyle}">
<Setter Property="Margin" Value="0,0,12,12"/>
</Style>
</GridView.ItemContainerStyle>
Margin="16,16,6,-6"
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{StaticResource CardBackgroundFillColorDefault}">
<StackPanel>
<shvc:ItemIcon
Icon="{Binding Icon}"
Quality="{Binding Quality}"/>
<TextBlock
Margin="0,0,0,2"
HorizontalAlignment="Center"
Text="{Binding Rate}"/>
</StackPanel>
</Border>
<shvc:BottomTextControl Text="{Binding Rate}">
<shvc:ItemIcon
Icon="{Binding Icon}"
Quality="{Binding Quality}"/>
</shvc:BottomTextControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</GridView>
@@ -95,25 +80,15 @@
<GridView
SelectionMode="None"
ItemsSource="{Binding Avatars}"
Margin="12,12,0,-12">
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem" BasedOn="{StaticResource DefaultGridViewItemStyle}">
<Setter Property="Margin" Value="0,0,12,12"/>
</Style>
</GridView.ItemContainerStyle>
Margin="16,16,6,-6"
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{StaticResource CardBackgroundFillColorDefault}">
<StackPanel>
<shvc:ItemIcon
Icon="{Binding Icon}"
Quality="{Binding Quality}"/>
<TextBlock
Margin="0,0,0,2"
HorizontalAlignment="Center"
Text="{Binding Rate}"/>
</StackPanel>
</Border>
<shvc:BottomTextControl Text="{Binding Rate}">
<shvc:ItemIcon
Icon="{Binding Icon}"
Quality="{Binding Quality}"/>
</shvc:BottomTextControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</GridView>
@@ -128,7 +103,7 @@
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Margin="12,0,12,0">
<Grid Margin="16,0,16,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="48"/>
<ColumnDefinition />
@@ -152,6 +127,7 @@
</Grid>
<ScrollViewer Grid.Row="1">
<ItemsControl
Margin="0,0,0,8"
HorizontalContentAlignment="Stretch"
ItemsSource="{Binding AvatarConstellationInfos}">
<ItemsControl.ItemsPanel>
@@ -164,7 +140,7 @@
<Border
Background="{StaticResource CardBackgroundFillColorDefault}"
CornerRadius="{StaticResource CompatCornerRadius}"
Margin="12,0,12,12">
Margin="16,0,16,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>

View File

@@ -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>
@@ -21,23 +34,27 @@
<RowDefinition/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<ScrollViewer Grid.Column="0" CanContentRenderOutsideBounds="True">
<ScrollViewer Grid.RowSpan="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MaxWidth="800"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<StackPanel Margin="32,0,24,24">
<StackPanel Margin="16,-16,16,16">
<sc:SettingsGroup Header="常规" Margin="0,0,0,0">
<sc:Setting
Icon="&#xE8AB;"
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="&#xE723;"
Command="{Binding DataContext.AttachGameAccountCommand,Source={StaticResource BindingProxy}}"
CommandParameter="{Binding}"
FontFamily="{StaticResource SymbolThemeFontFamily}"/>
<Button
Margin="4,8"
MinWidth="48"
VerticalAlignment="Stretch"
ToolTipService.ToolTip="重命名"
Content="&#xE8AC;"
Command="{Binding DataContext.ModifyGameAccountCommand,Source={StaticResource BindingProxy}}"
CommandParameter="{Binding}"
FontFamily="{StaticResource SymbolThemeFontFamily}"/>
<Button
Margin="4,8"
MinWidth="48"
VerticalAlignment="Stretch"
ToolTipService.ToolTip="删除"
Content="&#xE74D;"
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="&#xE785;"
Header="Unlock frame rate limit"
Description="Requires administrator privilege.&#10;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"/>
@@ -140,14 +246,21 @@
</ScrollViewer>
<Grid
Grid.Row="1"
VerticalAlignment="Bottom"
Background="{StaticResource SystemControlAcrylicElementMediumHighBrush}">
VerticalAlignment="Bottom">
<Button
Style="{StaticResource AccentButtonStyle}"
Command="{Binding LaunchCommand}"
HorizontalAlignment="Right"
Grid.Column="3"
Margin="24"
Width="138"
Content="启动游戏"/>
MinWidth="80"
Width="100"
Height="80">
<StackPanel>
<FontIcon Glyph="&#xE7FC;" FontSize="36"/>
<TextBlock Margin="0,4,0,0" Text="启动游戏"/>
</StackPanel>
</Button>
</Grid>
</Grid>
</control:ScopedPage>
</shc:ScopedPage>

View File

@@ -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}] 更新成功");

View File

@@ -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>();

View File

@@ -23,17 +23,22 @@
<ColumnDefinition MaxWidth="1000"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<StackPanel Margin="32,0,24,24">
<StackPanel Margin="16,-16,24,16">
<sc:SettingsGroup Header="关于 胡桃">
<Grid Margin="0,4,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image
Grid.Column="0"
<Border
Width="80"
Source="ms-appx:///Assets/Square150x150Logo.scale-200.png"/>
BorderThickness="1"
CornerRadius="{StaticResource CompatCornerRadius}"
BorderBrush="{StaticResource CardStrokeColorDefault}"
Background="{StaticResource CardBackgroundFillColorDefault}">
<Image Source="ms-appx:///Assets/Square150x150Logo.scale-200.png"/>
</Border>
<Grid
Margin="16,0,0,0"
Grid.Column="1">
@@ -56,10 +61,10 @@
<sc:Setting
Icon="&#xED15;"
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 +80,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 +114,7 @@
</sc:Setting.ActionContent>
</sc:Setting>
</sc:SettingsGroup>
<sc:SettingsGroup Header="测试功能">
<sc:Setting
Icon="&#xEC25;"
@@ -134,7 +143,6 @@
</sc:Setting.ActionContent>
</sc:Setting>
</sc:SettingsGroup>
</StackPanel>
</Grid>

Some files were not shown because too many files have changed in this diff Show More