mirror of
https://github.com/wangdage12/Snap.Hutao.git
synced 2026-02-18 02:42:15 +08:00
Compare commits
12 Commits
56c36a01ae
...
1.18.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb6d728c35 | ||
|
|
f87b80cc9e | ||
|
|
4b313b134e | ||
|
|
0c775a5d3d | ||
|
|
00cd5a8c07 | ||
|
|
d93ae2bb83 | ||
|
|
2f148488f4 | ||
|
|
df92894307 | ||
|
|
5fad9ad855 | ||
|
|
1ed2f4f29e | ||
|
|
db6df72791 | ||
|
|
bd9f188ac1 |
45
README.md
45
README.md
@@ -4,7 +4,7 @@
|
||||
**中文**
|
||||
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。
|
||||
|
||||
该版本注入功能暂不可用,并且由于缺失资源和开发能力,不建议长期使用
|
||||
自带的注入功能只有FPS调整,只保证FPS调整长期可用,你可以使用`注入选项`下方的第三方工具来使用更多功能,本项目提供的所有注入功能都不会影响游戏的公平性。
|
||||
|
||||
有条件的话可以加入discord服务器:https://discord.gg/ucH3mgeWpQ
|
||||
|
||||
@@ -15,8 +15,6 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
|
||||
|
||||
## 🚀 安装 / Installation
|
||||
|
||||
> 如果你的设备不支持ipv6,请下载末尾带有`ipv4`的压缩包,正常情况下请尽量下载普通包(服务器速度快)
|
||||
|
||||
目前 Sanp.Hutao.Rev 更新了打包方式,并采用了标准现代的 msi 安装,方便程序获取管理员权限和更多的功能设置,不再需要原 Depolyment
|
||||
|
||||
只有`.msi`安装包安装的可以和之前的版本共存,如果通过`.msix`安装包安装则可能出现`0x80073CF3`,备份旧版本数据文件夹后卸载旧版本即可继续安装,将旧版本数据文件夹里面的文件复制到该版本的数据文件夹中即可恢复数据
|
||||
@@ -34,7 +32,7 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
|
||||
|
||||
**目前元数据的编写进度:**
|
||||
|
||||
| 项目(V6.2) | 是否完成 |
|
||||
| 项目(V6.3) | 是否完成 |
|
||||
| ----------- | ----------- |
|
||||
| 总体数据 | ✔️ |
|
||||
|
||||
@@ -52,40 +50,55 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
|
||||
https://deepwiki.com/DGP-Studio/Snap.Hutao
|
||||
|
||||
https://deepwiki.com/DGP-Studio/Snap.Hutao.Server
|
||||
|
||||
**该项目所需的其他仓库,欢迎贡献或者自部署**
|
||||
|
||||
- 元数据:[Snap.Metadata](https://github.com/wangdage12/Snap.Metadata)
|
||||
- 服务端:[Snap.Server](https://github.com/wangdage12/Snap.Server)
|
||||
- Web管理后台和官网:[Snap.Server.Web](https://github.com/wangdage12/Snap.Server.Web)
|
||||
|
||||
## 打包测试
|
||||
|
||||
由于采用了 wix 进行打包程序,VS 需要安装 **HeatWave for VS2022**(2026兼容)。需要 msi 安装包时,右键选中 Snap.Hutao.Installer 生成后即可在目标目录找到。默认目录:Snap.Hutao.Installer\bin\x64\Release\en-US\Snap.Hutao.Installer.msi
|
||||
|
||||
### 资源
|
||||
## 资源和服务器状态
|
||||
|
||||
> 注意:普通包的资源服务器只能使用ipv6连接,也就是说,你的电脑必须有ipv6,并且建议你手动配置DNS为`223.5.5.5`
|
||||
> 如果你的设备不支持ipv6,请下载末尾带有`ipv4`的压缩包
|
||||
> 由于数据文件夹中有元数据的仓库和图片缓存,才得以恢复资源文件
|
||||
> 如果你发现之前版本可以显示的图片不能显示了,请查找旧数据文件夹
|
||||
> `C:\Users\<用户名>\AppData\Local\Packages\xxxDGPStudio.SnapHutao_xxx\LocalCache\ImageCache`
|
||||
> 并将`ImageCache`文件夹提供给我,我会尽力恢复资源
|
||||
|
||||
[服务器状态页面](http://serverjp.wdg.cloudns.ch:3001/status/hts)
|
||||
<a href="https://uptimerobot.com" target="_blank" rel="noopener">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)"
|
||||
srcset="https://raw.githubusercontent.com/wangdage12/wangdage12/main/assets/uptimerobot-logo.svg">
|
||||
<img alt="logo"
|
||||
src="https://raw.githubusercontent.com/wangdage12/wangdage12/main/assets/uptimerobot-logo-dark.svg" width="300">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
我们将使用[UptimeRobot](https://uptimerobot.com)赞助的监控服务作为新的服务器状态页面,它有更多的功能
|
||||
|
||||
[新服务器状态页面](https://stats.uptimerobot.com/fHxWxdxK61)
|
||||
|
||||
[旧服务器状态页面](http://serverjp.wdg.cloudns.ch:3001/status/hts)
|
||||
|
||||
---
|
||||
|
||||
**元数据仓库:**
|
||||
https://github.com/wangdage12/Snap.Metadata
|
||||
|
||||
镜像:
|
||||
仓库镜像:
|
||||

|
||||
|
||||
http://htgit.wdg.cloudns.ch/wdg1122/Snap.Metadata
|
||||
|
||||
---
|
||||
|
||||
**临时API:**
|
||||
**API:**
|
||||
|
||||

|
||||
|
||||
https://htserver.wdg.cloudns.ch/api/
|
||||
|
||||
|
||||
---
|
||||
|
||||
**临时资源站:**
|
||||
**图片资源站:**
|
||||
|
||||
https://htserver.wdg.cloudns.ch/
|
||||
|
||||
Binary file not shown.
@@ -4,5 +4,8 @@ This file contains the declaration of all the localizable strings.
|
||||
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US">
|
||||
|
||||
<String Id="DowngradeError" Value="A newer version of [ProductName] is already installed." />
|
||||
<String Id="MainAppTitle" Value="Snap.Hutao" />
|
||||
<String Id="DesktopShortcutTitle" Value="Desktop Shortcut" />
|
||||
<String Id="StartMenuShortcutTitle" Value="Start Menu Shortcut" />
|
||||
|
||||
</WixLocalization>
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
|
||||
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
|
||||
<Package
|
||||
Name="Snap.Hutao"
|
||||
Manufacturer="Millennium Science Technology R-D Inst"
|
||||
Version="1.17.4.0"
|
||||
Version="1.18.3.0"
|
||||
UpgradeCode="121203be-60cb-408f-92cc-7080f6598e68"
|
||||
Language="2052"
|
||||
Scope="perMachine">
|
||||
|
||||
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
|
||||
<Property Id="ApplicationFolderName" Value="Snap.Hutao" />
|
||||
<Property Id="WixAppFolder" Value="WixPerMachineFolder" />
|
||||
|
||||
<MajorUpgrade DowngradeErrorMessage="!(loc.DowngradeError)" />
|
||||
<MediaTemplate EmbedCab="yes" />
|
||||
|
||||
<Feature Id="ProductFeature" Title="Snap.Hutao" Level="1">
|
||||
<ComponentGroupRef Id="MainAppComponents" />
|
||||
<ui:WixUI Id="WixUI_InstallDir" InstallDirectory="INSTALLFOLDER" />
|
||||
|
||||
<!-- 快捷方式组件 -->
|
||||
<ComponentRef Id="ApplicationShortcut" />
|
||||
<Feature Id="MainApp" Title="!(loc.MainAppTitle)" Level="1">
|
||||
<ComponentGroupRef Id="MainAppComponents" />
|
||||
</Feature>
|
||||
|
||||
<Feature Id="DesktopShortcutFeature" Title="!(loc.DesktopShortcutTitle)" Level="1">
|
||||
<ComponentRef Id="DesktopShortcut" />
|
||||
</Feature>
|
||||
|
||||
<Feature Id="StartMenuShortcutFeature" Title="!(loc.StartMenuShortcutTitle)" Level="1">
|
||||
<ComponentRef Id="ApplicationShortcut" />
|
||||
</Feature>
|
||||
</Package>
|
||||
|
||||
<!-- 安装目录 -->
|
||||
|
||||
11
src/Snap.Hutao/Snap.Hutao.Installer/Package.zh-cn.wxl
Normal file
11
src/Snap.Hutao/Snap.Hutao.Installer/Package.zh-cn.wxl
Normal file
@@ -0,0 +1,11 @@
|
||||
<!--
|
||||
This file contains the declaration of all the localizable strings.
|
||||
-->
|
||||
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="zh-CN">
|
||||
|
||||
<String Id="DowngradeError" Value="已安装更新版本的 [ProductName]。" />
|
||||
<String Id="MainAppTitle" Value="Snap.Hutao" />
|
||||
<String Id="DesktopShortcutTitle" Value="桌面快捷方式" />
|
||||
<String Id="StartMenuShortcutTitle" Value="开始菜单快捷方式" />
|
||||
|
||||
</WixLocalization>
|
||||
@@ -4,6 +4,8 @@
|
||||
<Platform>x64</Platform>
|
||||
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
|
||||
<Configuration>Release</Configuration>
|
||||
<DefaultCulture>zh-CN</DefaultCulture>
|
||||
<Cultures>zh-CN;en-US</Cultures>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -19,6 +21,13 @@
|
||||
<SuppressRootDirectory>true</SuppressRootDirectory>
|
||||
</HarvestDirectory>
|
||||
|
||||
<PackageReference Include="WixToolset.Heat" Version="4.0.1" />
|
||||
<PackageReference Include="WixToolset.Heat" Version="6.0.2" />
|
||||
<PackageReference Include="WixToolset.UI.wixext" Version="6.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<WixLocalization Include="Package.zh-cn.wxl" />
|
||||
<WixLocalization Include="Package.en-us.wxl" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -39,6 +39,7 @@ internal static class DependencyInjection
|
||||
.AddJsonOptions()
|
||||
.AddDatabase()
|
||||
.AddServices()
|
||||
.AddThirdPartyToolService()
|
||||
.AddResponseValidation()
|
||||
.AddConfiguredHttpClients()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.Text.Json;
|
||||
using Snap.Hutao.Factory.Process;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using Snap.Hutao.Service.ThirdPartyTool;
|
||||
using Snap.Hutao.Win32;
|
||||
using System.Data.Common;
|
||||
|
||||
@@ -66,5 +67,10 @@ internal static partial class ServiceCollectionExtension
|
||||
.UseSqlite(sqlConnectionString);
|
||||
}
|
||||
}
|
||||
|
||||
public IServiceCollection AddThirdPartyToolService()
|
||||
{
|
||||
return services.AddSingleton<IThirdPartyToolService, ThirdPartyToolService>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,8 @@ internal static class HutaoRuntime
|
||||
|
||||
public static WebView2Version WebView2Version { get; } = InitializeWebView2();
|
||||
|
||||
public static string WebView2UserDataDirectory { get; } = InitializeWebView2UserDataDirectory();
|
||||
|
||||
// ⚠️ 延迟初始化以避免循环依赖
|
||||
private static readonly Lazy<bool> LazyIsProcessElevated = new(GetIsProcessElevated);
|
||||
|
||||
@@ -144,6 +146,13 @@ internal static class HutaoRuntime
|
||||
return cacheDir;
|
||||
}
|
||||
|
||||
private static string InitializeWebView2UserDataDirectory()
|
||||
{
|
||||
string directory = Path.Combine(LocalCacheDirectory, "WebView2");
|
||||
Directory.CreateDirectory(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
private static bool CheckAppNotificationEnabled()
|
||||
{
|
||||
try
|
||||
@@ -226,4 +235,4 @@ internal static class HutaoRuntime
|
||||
return new(string.Empty, SH.CoreWebView2HelperVersionUndetected, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,9 @@ internal static class AvatarIds
|
||||
public static readonly AvatarId Nefer = 10000122;
|
||||
public static readonly AvatarId Durin = 10000123;
|
||||
public static readonly AvatarId Jahoda = 10000124;
|
||||
public static readonly AvatarId Columbina = 10000125;
|
||||
public static readonly AvatarId Zibai = 10000126;
|
||||
public static readonly AvatarId Illuga = 10000127;
|
||||
|
||||
private static readonly FrozenSet<AvatarId> StandardWishIds =
|
||||
[
|
||||
|
||||
@@ -22,9 +22,8 @@ internal static class WeaponIds
|
||||
11401U, 11402U, 11403U, 11405U,
|
||||
12401U, 12402U, 12403U, 12405U,
|
||||
13401U, 13407U,
|
||||
14401U, 14402U, 14403U, 14409U,
|
||||
15401U, 15402U, 15403U, 15405U,
|
||||
15434U
|
||||
14401U, 14402U, 14403U, 14409U, 14433U, 14434U,
|
||||
15401U, 15402U, 15403U, 15405U, 15434U
|
||||
];
|
||||
|
||||
public static readonly FrozenSet<WeaponId> OrangeStandardWishIds =
|
||||
@@ -34,7 +33,8 @@ internal static class WeaponIds
|
||||
13502U, 13505U,
|
||||
14501U, 14502U,
|
||||
15501U, 15502U,
|
||||
15515U, 11518U
|
||||
15515U, 11518U,
|
||||
14522U, 11519U
|
||||
];
|
||||
|
||||
public static bool IsOrangeStandardWish(in WeaponId weaponId)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<Identity
|
||||
Name="60568DGPStudio.SnapHutao"
|
||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||
Version="1.18.0.0" />
|
||||
Version="1.18.3.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Snap Hutao</DisplayName>
|
||||
|
||||
@@ -1208,6 +1208,12 @@
|
||||
<data name="ServiceYaeWaitForGameResponseMessage" xml:space="preserve">
|
||||
<value>正在等待游戏数据</value>
|
||||
</data>
|
||||
<data name="ServiceThirdPartyToolNoExecutableFound" xml:space="preserve">
|
||||
<value>未找到可执行文件</value>
|
||||
</data>
|
||||
<data name="ServiceThirdPartyToolFileNotFound" xml:space="preserve">
|
||||
<value>文件不存在:{0}</value>
|
||||
</data>
|
||||
<data name="UIViewMainTitleBarBackgroundActivityAction" xml:space="preserve">
|
||||
<value>后台任务</value>
|
||||
</data>
|
||||
@@ -1586,6 +1592,12 @@
|
||||
<data name="ViewDialogLaunchGamePackageConvertTitle" xml:space="preserve">
|
||||
<value>正在转换客户端</value>
|
||||
</data>
|
||||
<data name="ViewDialogThirdPartyToolDescription" xml:space="preserve">
|
||||
<value>工具描述:</value>
|
||||
</data>
|
||||
<data name="ViewDialogThirdPartyToolLaunch" xml:space="preserve">
|
||||
<value>启动</value>
|
||||
</data>
|
||||
<data name="ViewDialogQRCodeTitle" xml:space="preserve">
|
||||
<value>使用米游社扫描二维码</value>
|
||||
</data>
|
||||
@@ -2916,6 +2928,9 @@
|
||||
<data name="ViewPageLaunchGameInjectionHeader" xml:space="preserve">
|
||||
<value>注入</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameThirdPartyTools" xml:space="preserve">
|
||||
<value>第三方注入工具:</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameIslandConnected" xml:space="preserve">
|
||||
<value>已连接到游戏,更改设置将会动态反映到游戏中</value>
|
||||
</data>
|
||||
|
||||
@@ -139,6 +139,13 @@ internal sealed class GameFpsUnlockInterop : IGameIslandInterop, IDisposable
|
||||
category: "fps.unlocker",
|
||||
level: Sentry.BreadcrumbLevel.Info);
|
||||
|
||||
// 构建游戏启动参数,传递给 unlockfps.exe
|
||||
string gameArguments = BuildGameArguments(context);
|
||||
SentrySdk.AddBreadcrumb(
|
||||
$"Game arguments for unlocker: {gameArguments}",
|
||||
category: "fps.unlocker",
|
||||
level: Sentry.BreadcrumbLevel.Info);
|
||||
|
||||
ProcessStartInfo startInfo = new()
|
||||
{
|
||||
FileName = unlockerPath,
|
||||
@@ -148,11 +155,12 @@ internal sealed class GameFpsUnlockInterop : IGameIslandInterop, IDisposable
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
WindowStyle = ProcessWindowStyle.Normal,
|
||||
Arguments = gameArguments,
|
||||
};
|
||||
|
||||
unlockerProcess = new Process { StartInfo = startInfo };
|
||||
|
||||
|
||||
|
||||
unlockerProcess.Start();
|
||||
|
||||
|
||||
@@ -170,7 +178,7 @@ internal sealed class GameFpsUnlockInterop : IGameIslandInterop, IDisposable
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Task errorTask = Task.Run(async () =>
|
||||
{
|
||||
while (!unlockerProcess.StandardError.EndOfStream)
|
||||
@@ -197,6 +205,53 @@ internal sealed class GameFpsUnlockInterop : IGameIslandInterop, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildGameArguments(BeforeLaunchExecutionContext context)
|
||||
{
|
||||
LaunchOptions launchOptions = context.LaunchOptions;
|
||||
|
||||
if (!launchOptions.AreCommandLineArgumentsEnabled.Value)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
StringBuilder arguments = new();
|
||||
|
||||
// 构建与 GameProcessFactory.CreateForDefault 相同的命令行参数
|
||||
if (launchOptions.IsBorderless.Value)
|
||||
{
|
||||
arguments.Append(" -popupwindow");
|
||||
}
|
||||
|
||||
if (launchOptions.IsExclusive.Value)
|
||||
{
|
||||
arguments.Append(" -window-mode exclusive");
|
||||
}
|
||||
|
||||
arguments.Append($" -screen-fullscreen {(launchOptions.IsFullScreen.Value ? "1" : "0")}");
|
||||
|
||||
if (launchOptions.IsScreenWidthEnabled.Value)
|
||||
{
|
||||
arguments.Append($" -screen-width {launchOptions.ScreenWidth.Value}");
|
||||
}
|
||||
|
||||
if (launchOptions.IsScreenHeightEnabled.Value)
|
||||
{
|
||||
arguments.Append($" -screen-height {launchOptions.ScreenHeight.Value}");
|
||||
}
|
||||
|
||||
if (launchOptions.IsMonitorEnabled.Value)
|
||||
{
|
||||
arguments.Append($" -monitor {launchOptions.Monitor.Value?.Value ?? 1}");
|
||||
}
|
||||
|
||||
if (launchOptions.IsPlatformTypeEnabled.Value)
|
||||
{
|
||||
arguments.Append($" -platform_type {launchOptions.PlatformType.Value:G}");
|
||||
}
|
||||
|
||||
return arguments.ToString();
|
||||
}
|
||||
|
||||
private async ValueTask MonitorExistingUnlockerAsync(LaunchExecutionContext context, CancellationToken token)
|
||||
{
|
||||
// 恢复模式下,检查是否有解锁器进程在运行
|
||||
|
||||
@@ -21,6 +21,8 @@ internal abstract class AbstractLaunchExecutionInvoker
|
||||
private bool invoked;
|
||||
|
||||
protected ImmutableArray<ILaunchExecutionHandler> Handlers { get; init; }
|
||||
protected virtual bool ShouldWaitForProcessExit { get => true; }
|
||||
protected virtual bool ShouldSpinWaitGameExitAfterInvoke { get => true; }
|
||||
|
||||
public static bool Invoking()
|
||||
{
|
||||
@@ -40,7 +42,7 @@ internal abstract class AbstractLaunchExecutionInvoker
|
||||
finally
|
||||
{
|
||||
Invokers.TryRemove(this, out _);
|
||||
if (!Invoking())
|
||||
if (!Invoking() && ShouldSpinWaitGameExitAfterInvoke)
|
||||
{
|
||||
await GameLifeCycle.SpinWaitGameExitAsync(taskContext).ConfigureAwait(false);
|
||||
}
|
||||
@@ -132,7 +134,7 @@ internal abstract class AbstractLaunchExecutionInvoker
|
||||
}
|
||||
|
||||
// 只有在没有启用Island且进程存在时才等待退出
|
||||
if (process is { IsRunning: true })
|
||||
if (ShouldWaitForProcessExit && process is { IsRunning: true })
|
||||
{
|
||||
progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit));
|
||||
try
|
||||
@@ -148,7 +150,7 @@ internal abstract class AbstractLaunchExecutionInvoker
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (beforeContext.LaunchOptions.IsIslandEnabled.Value)
|
||||
else if (ShouldWaitForProcessExit && beforeContext.LaunchOptions.IsIslandEnabled.Value)
|
||||
{
|
||||
progress.Report(new(SH.ServiceGameLaunchPhaseWaitingProcessExit));
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
@@ -170,4 +172,4 @@ internal abstract class AbstractLaunchExecutionInvoker
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ namespace Snap.Hutao.Service.Game.Launching.Invoker;
|
||||
|
||||
internal sealed class ConvertOnlyLaunchExecutionInvoker : AbstractLaunchExecutionInvoker
|
||||
{
|
||||
protected override bool ShouldWaitForProcessExit { get => false; }
|
||||
protected override bool ShouldSpinWaitGameExitAfterInvoke { get => false; }
|
||||
|
||||
public ConvertOnlyLaunchExecutionInvoker()
|
||||
{
|
||||
Handlers =
|
||||
@@ -24,4 +27,4 @@ internal sealed class ConvertOnlyLaunchExecutionInvoker : AbstractLaunchExecutio
|
||||
// Since this invoker is only for conversion, we do not actually need the process.
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ internal sealed partial class HutaoAsAService : IHutaoAsAService
|
||||
}
|
||||
}
|
||||
|
||||
// Filter announcements by Distribution
|
||||
array = [.. array.Where(a => string.IsNullOrEmpty(a.Distribution) || a.Distribution == "Snap Hutao")]; // 请自行修改发行版名称
|
||||
|
||||
foreach (HutaoAnnouncement item in array)
|
||||
{
|
||||
item.DismissCommand = dismissCommand;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using Snap.Hutao.Web.ThirdPartyTool;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Snap.Hutao.Service.ThirdPartyTool;
|
||||
|
||||
internal interface IThirdPartyToolService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取第三方工具列表
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>工具列表</returns>
|
||||
ValueTask<ImmutableArray<ToolInfo>> GetToolsAsync(CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 下载工具文件
|
||||
/// </summary>
|
||||
/// <param name="tool">工具信息</param>
|
||||
/// <param name="progress">进度报告</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>是否下载成功</returns>
|
||||
ValueTask<bool> DownloadToolAsync(ToolInfo tool, IProgress<double>? progress = null, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 启动工具
|
||||
/// </summary>
|
||||
/// <param name="tool">工具信息</param>
|
||||
/// <returns>是否启动成功</returns>
|
||||
ValueTask<bool> LaunchToolAsync(ToolInfo tool);
|
||||
|
||||
/// <summary>
|
||||
/// 检查工具是否已下载
|
||||
/// </summary>
|
||||
/// <param name="tool">工具信息</param>
|
||||
/// <returns>是否已下载</returns>
|
||||
bool IsToolDownloaded(ToolInfo tool);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using Snap.Hutao.Web.ThirdPartyTool;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Service.ThirdPartyTool;
|
||||
|
||||
[HttpClient(HttpClientConfiguration.Default)]
|
||||
[Service(ServiceLifetime.Singleton, typeof(IThirdPartyToolService))]
|
||||
internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
|
||||
{
|
||||
private const string ApiBaseUrl = "https://htserver.wdg.cloudns.ch/api";
|
||||
private const string ToolsEndpoint = "/tools";
|
||||
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
|
||||
private readonly IMessenger messenger;
|
||||
|
||||
[GeneratedConstructor]
|
||||
public partial ThirdPartyToolService(IServiceProvider serviceProvider, HttpClient httpClient);
|
||||
|
||||
public async ValueTask<ImmutableArray<ToolInfo>> GetToolsAsync(CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpClient httpClient = httpClientFactory.CreateClient();
|
||||
|
||||
// 添加日志
|
||||
SentrySdk.AddBreadcrumb($"Creating request to: {ApiBaseUrl}{ToolsEndpoint}", category: "ThirdPartyTool");
|
||||
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri($"{ApiBaseUrl}{ToolsEndpoint}")
|
||||
.Get();
|
||||
|
||||
SentrySdk.AddBreadcrumb($"Sending HTTP request", category: "ThirdPartyTool");
|
||||
|
||||
ToolApiResponse? response = await builder
|
||||
.SendAsync<ToolApiResponse>(httpClient, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
SentrySdk.AddBreadcrumb($"Request completed", category: "ThirdPartyTool");
|
||||
|
||||
if (response is null)
|
||||
{
|
||||
SentrySdk.AddBreadcrumb("Response is null", category: "ThirdPartyTool");
|
||||
return ImmutableArray<ToolInfo>.Empty;
|
||||
}
|
||||
|
||||
SentrySdk.AddBreadcrumb($"Response received: Code={response.Code}, Message={response.Message}, Data.Length={response.Data.Length}", category: "ThirdPartyTool");
|
||||
|
||||
if (response.Code != 0)
|
||||
{
|
||||
SentrySdk.AddBreadcrumb($"API returned error code: {response.Code}, Message: {response.Message}", category: "ThirdPartyTool");
|
||||
return ImmutableArray<ToolInfo>.Empty;
|
||||
}
|
||||
|
||||
return response.Data;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
SentrySdk.AddBreadcrumb($"HTTP request failed: {ex.Message}", category: "ThirdPartyTool");
|
||||
SentrySdk.CaptureException(ex);
|
||||
return ImmutableArray<ToolInfo>.Empty;
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
SentrySdk.AddBreadcrumb($"Request timed out or was cancelled: {ex.Message}", category: "ThirdPartyTool");
|
||||
SentrySdk.CaptureException(ex);
|
||||
return ImmutableArray<ToolInfo>.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SentrySdk.AddBreadcrumb($"Failed to get third party tools: {ex.Message}", category: "ThirdPartyTool");
|
||||
SentrySdk.CaptureException(ex);
|
||||
return ImmutableArray<ToolInfo>.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<bool> DownloadToolAsync(ToolInfo tool, IProgress<double>? progress = null, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
string toolDirectory = GetToolDirectory(tool);
|
||||
Directory.CreateDirectory(toolDirectory);
|
||||
|
||||
int totalFiles = tool.Files.Count;
|
||||
int downloadedFiles = 0;
|
||||
|
||||
using (HttpClient httpClient = httpClientFactory.CreateClient())
|
||||
{
|
||||
foreach (string fileName in tool.Files)
|
||||
{
|
||||
string fileUrl = $"{tool.Url}{fileName}";
|
||||
string localFilePath = Path.Combine(toolDirectory, fileName);
|
||||
|
||||
// 如果文件已存在,跳过下载
|
||||
if (File.Exists(localFilePath))
|
||||
{
|
||||
downloadedFiles++;
|
||||
progress?.Report((double)downloadedFiles / totalFiles * 100);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
HttpResponseMessage response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using (Stream contentStream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
|
||||
using (FileStream fileStream = new(localFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
{
|
||||
await contentStream.CopyToAsync(fileStream, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
downloadedFiles++;
|
||||
progress?.Report((double)downloadedFiles / totalFiles * 100);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
messenger.Send(InfoBarMessage.Error(ex));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<bool> LaunchToolAsync(ToolInfo tool)
|
||||
{
|
||||
try
|
||||
{
|
||||
string toolDirectory = GetToolDirectory(tool);
|
||||
|
||||
// 查找可执行文件(.exe)
|
||||
string? executablePath = tool.Files.FirstOrDefault(f => f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase));
|
||||
if (string.IsNullOrEmpty(executablePath))
|
||||
{
|
||||
messenger.Send(InfoBarMessage.Warning(SH.ServiceThirdPartyToolNoExecutableFound));
|
||||
return false;
|
||||
}
|
||||
|
||||
string fullPath = Path.Combine(toolDirectory, executablePath);
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
messenger.Send(InfoBarMessage.Warning(SH.FormatServiceThirdPartyToolFileNotFound(fullPath)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 尝试以管理员权限启动
|
||||
ProcessStartInfo startInfo = new()
|
||||
{
|
||||
FileName = fullPath,
|
||||
WorkingDirectory = toolDirectory,
|
||||
UseShellExecute = true,
|
||||
Verb = "runas", // 请求管理员权限
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(startInfo);
|
||||
}
|
||||
catch (System.ComponentModel.Win32Exception)
|
||||
{
|
||||
// 用户拒绝了管理员权限,尝试以普通权限启动
|
||||
startInfo.Verb = string.Empty;
|
||||
startInfo.UseShellExecute = false;
|
||||
Process.Start(startInfo);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
messenger.Send(InfoBarMessage.Error(ex));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsToolDownloaded(ToolInfo tool)
|
||||
{
|
||||
string toolDirectory = GetToolDirectory(tool);
|
||||
if (!Directory.Exists(toolDirectory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查所有文件是否存在
|
||||
foreach (string fileName in tool.Files)
|
||||
{
|
||||
string filePath = Path.Combine(toolDirectory, fileName);
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string GetToolDirectory(ToolInfo tool)
|
||||
{
|
||||
// 使用数据目录/工具名作为存储路径
|
||||
return Path.Combine(HutaoRuntime.DataDirectory, "Tools", tool.Name);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<UseWPF>False</UseWPF>
|
||||
<!-- 配置版本号 -->
|
||||
<Version>1.18.0.0</Version>
|
||||
<Version>1.18.3.0</Version>
|
||||
|
||||
<UseWindowsForms>False</UseWindowsForms>
|
||||
<ImplicitUsings>False</ImplicitUsings>
|
||||
|
||||
@@ -20,6 +20,8 @@ internal partial class ScopedPage : Page
|
||||
|
||||
protected ScopedPage()
|
||||
{
|
||||
// Allow a small set of recent pages to be cached to reduce navigation stutter.
|
||||
NavigationCacheMode = NavigationCacheMode.Enabled;
|
||||
// Events/Override Methods order
|
||||
// ----------------------------------------------------------------------
|
||||
// Page Navigation methods:
|
||||
@@ -103,6 +105,13 @@ internal partial class ScopedPage : Page
|
||||
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// When navigation cache is enabled, the page instance is reused.
|
||||
// Do not tear down DataContext/scope here to avoid invalid state on return.
|
||||
if (NavigationCacheMode != NavigationCacheMode.Disabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel all tasks executed by the view model
|
||||
viewCts.Cancel();
|
||||
|
||||
@@ -140,4 +149,4 @@ internal partial class ScopedPage : Page
|
||||
|
||||
Unloaded -= OnUnloaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<ContentDialog
|
||||
x:Class="Snap.Hutao.UI.Xaml.View.Dialog.ThirdPartyToolDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:shuxm="using:Snap.Hutao.UI.Xaml.Markup"
|
||||
Title="{x:Bind Tool.Name, Mode=OneWay}"
|
||||
CloseButtonText="{shuxm:ResourceString Name=ContentDialogCancelCloseButtonText}"
|
||||
DefaultButton="Primary"
|
||||
PrimaryButtonText="{shuxm:ResourceString Name=ViewDialogThirdPartyToolLaunch}"
|
||||
Style="{StaticResource DefaultContentDialogStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid RowSpacing="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{shuxm:ResourceString Name=ViewDialogThirdPartyToolDescription}"
|
||||
TextWrapping="Wrap"/>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{x:Bind Tool.Description, Mode=OneWay}"
|
||||
TextWrapping="Wrap"/>
|
||||
|
||||
<ProgressBar
|
||||
Grid.Row="2"
|
||||
Height="4"
|
||||
IsIndeterminate="{x:Bind IsDownloading.Value, Mode=OneWay, FallbackValue=False}"
|
||||
Visibility="{x:Bind IsDownloading.Value, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
@@ -0,0 +1,77 @@
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using Snap.Hutao.Service.ThirdPartyTool;
|
||||
using Snap.Hutao.Web.ThirdPartyTool;
|
||||
|
||||
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
||||
|
||||
[DependencyProperty<ToolInfo>("Tool")]
|
||||
[DependencyProperty<bool>("IsDownloading", DefaultValue = false)]
|
||||
internal sealed partial class ThirdPartyToolDialog : ContentDialog
|
||||
{
|
||||
private readonly IContentDialogFactory contentDialogFactory;
|
||||
private readonly IThirdPartyToolService thirdPartyToolService;
|
||||
private readonly IMessenger messenger;
|
||||
|
||||
[GeneratedConstructor(InitializeComponent = true)]
|
||||
public partial ThirdPartyToolDialog(IServiceProvider serviceProvider);
|
||||
|
||||
public ThirdPartyToolDialog(IServiceProvider serviceProvider, ToolInfo tool)
|
||||
: this(serviceProvider)
|
||||
{
|
||||
Tool = tool;
|
||||
PrimaryButtonClick += OnPrimaryButtonClick;
|
||||
}
|
||||
|
||||
private void OnPrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
args.Cancel = true;
|
||||
HandleLaunchAsync().SafeForget();
|
||||
}
|
||||
|
||||
private async Task HandleLaunchAsync()
|
||||
{
|
||||
// 在 UI 线程上获取 Tool 的引用,避免后续跨线程访问依赖属性
|
||||
ToolInfo? tool = Tool;
|
||||
|
||||
try
|
||||
{
|
||||
IsDownloading = true;
|
||||
|
||||
// 检查工具是否已下载
|
||||
if (tool is not null && !thirdPartyToolService.IsToolDownloaded(tool))
|
||||
{
|
||||
// 下载工具
|
||||
bool downloadSuccess = await thirdPartyToolService.DownloadToolAsync(tool, null).ConfigureAwait(false);
|
||||
if (!downloadSuccess)
|
||||
{
|
||||
await contentDialogFactory.TaskContext.SwitchToMainThreadAsync();
|
||||
IsDownloading = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 启动工具
|
||||
if (tool is not null)
|
||||
{
|
||||
bool launchSuccess = await thirdPartyToolService.LaunchToolAsync(tool).ConfigureAwait(false);
|
||||
if (launchSuccess)
|
||||
{
|
||||
await contentDialogFactory.TaskContext.SwitchToMainThreadAsync();
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
messenger.Send(InfoBarMessage.Error(ex));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await contentDialogFactory.TaskContext.SwitchToMainThreadAsync();
|
||||
IsDownloading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,10 +264,13 @@
|
||||
<shuxv:UserView x:Name="UserView"/>
|
||||
</NavigationView.PaneFooter>
|
||||
|
||||
<Frame x:Name="ContentFrame" ContentTransitions="{StaticResource NavigationThemeTransitions}"/>
|
||||
<Frame
|
||||
x:Name="ContentFrame"
|
||||
CacheSize="5"
|
||||
ContentTransitions="{StaticResource NavigationThemeTransitions}"/>
|
||||
</NavigationView>
|
||||
</Grid>
|
||||
|
||||
<shuxv:InfoBarView Margin="0,44,0,0" VerticalAlignment="Stretch"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<shuxc:ScopedPage
|
||||
x:Class="Snap.Hutao.UI.Xaml.View.Page.LaunchGamePage"
|
||||
x:Name="PageRoot"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cw="using:CommunityToolkit.WinUI"
|
||||
@@ -11,6 +12,7 @@
|
||||
xmlns:shsg="using:Snap.Hutao.Service.Game"
|
||||
xmlns:shsgp="using:Snap.Hutao.Service.Game.PathAbstraction"
|
||||
xmlns:shux="using:Snap.Hutao.UI.Xaml"
|
||||
xmlns:shwt="using:Snap.Hutao.Web.ThirdPartyTool"
|
||||
xmlns:shuxb="using:Snap.Hutao.UI.Xaml.Behavior"
|
||||
xmlns:shuxba="using:Snap.Hutao.UI.Xaml.Behavior.Action"
|
||||
xmlns:shuxc="using:Snap.Hutao.UI.Xaml.Control"
|
||||
@@ -558,13 +560,49 @@
|
||||
Margin="16"
|
||||
Padding="0"
|
||||
cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
|
||||
<Grid Style="{ThemeResource AcrylicGridCardStyle}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Spacing="8">
|
||||
<!-- 第三方注入工具 -->
|
||||
<Grid Style="{ThemeResource AcrylicGridCardStyle}" Visibility="{Binding ThirdPartyTools.Value, Converter={StaticResource EmptyCollectionToVisibilityConverter}}">
|
||||
<Grid Padding="16,12" ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{shuxm:ResourceString Name=ViewPageLaunchGameThirdPartyTools}"/>
|
||||
<ItemsControl
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Left"
|
||||
ItemsSource="{Binding ThirdPartyTools.Value, Mode=OneWay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="shwt:ToolInfo">
|
||||
<Button
|
||||
Padding="12,6"
|
||||
Command="{Binding DataContext.ShowThirdPartyToolDialogCommand, ElementName=PageRoot}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="{Binding Name}"
|
||||
Style="{ThemeResource AccentButtonStyle}"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid Style="{ThemeResource AcrylicGridCardStyle}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Padding="16,8"
|
||||
@@ -923,6 +961,7 @@
|
||||
</ContentControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.UI.Input.LowLevel;
|
||||
@@ -337,7 +338,8 @@ internal sealed partial class CompactWebView2Window : Microsoft.UI.Xaml.Window,
|
||||
{
|
||||
AdditionalBrowserArguments = "--do-not-de-elevate --autoplay-policy=no-user-gesture-required",
|
||||
};
|
||||
CoreWebView2Environment environment = await CoreWebView2Environment.CreateWithOptionsAsync(null, null, options);
|
||||
string userDataFolder = HutaoRuntime.WebView2UserDataDirectory;
|
||||
CoreWebView2Environment environment = await CoreWebView2Environment.CreateWithOptionsAsync(null, userDataFolder, options);
|
||||
await WebView.EnsureCoreWebView2Async(environment);
|
||||
}
|
||||
catch (SEHException ex)
|
||||
@@ -430,4 +432,4 @@ internal sealed partial class CompactWebView2Window : Microsoft.UI.Xaml.Window,
|
||||
RefreshButton.Command = RefreshCommand;
|
||||
ProgressRing.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.UI;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.UI.Windowing;
|
||||
using Snap.Hutao.UI.Windowing.Abstraction;
|
||||
@@ -154,7 +155,8 @@ internal sealed partial class WebView2Window : Microsoft.UI.Xaml.Window,
|
||||
{
|
||||
AdditionalBrowserArguments = "--do-not-de-elevate",
|
||||
};
|
||||
CoreWebView2Environment environment = await CoreWebView2Environment.CreateWithOptionsAsync(null, null, options);
|
||||
string userDataFolder = HutaoRuntime.WebView2UserDataDirectory;
|
||||
CoreWebView2Environment environment = await CoreWebView2Environment.CreateWithOptionsAsync(null, userDataFolder, options);
|
||||
await WebView.EnsureCoreWebView2Async(environment);
|
||||
}
|
||||
catch (SEHException)
|
||||
@@ -213,4 +215,4 @@ internal sealed partial class WebView2Window : Microsoft.UI.Xaml.Window,
|
||||
{
|
||||
contentProvider.ActualTheme = sender.ActualTheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Copyright (c) Millennium-Science-Technology-R-D-Inst. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
@@ -16,11 +17,13 @@ using Snap.Hutao.Service.Game.PathAbstraction;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using Snap.Hutao.Service.ThirdPartyTool;
|
||||
using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.UI.Input.LowLevel;
|
||||
using Snap.Hutao.UI.Xaml.View.Dialog;
|
||||
using Snap.Hutao.UI.Xaml.View.Window;
|
||||
using Snap.Hutao.ViewModel.User;
|
||||
using Snap.Hutao.Web.ThirdPartyTool;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
|
||||
@@ -54,6 +57,9 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
|
||||
|
||||
public ImmutableArray<LaunchScheme> KnownSchemes { get; } = KnownLaunchSchemes.Values;
|
||||
|
||||
private IObservableProperty<ImmutableArray<ToolInfo>> thirdPartyToolsField = new ObservableProperty<ImmutableArray<ToolInfo>>(ImmutableArray<ToolInfo>.Empty);
|
||||
public IObservableProperty<ImmutableArray<ToolInfo>> ThirdPartyTools { get => thirdPartyToolsField; }
|
||||
|
||||
LaunchScheme? IViewModelSupportLaunchExecution.TargetScheme { get => TargetSchemeFilteredGameAccountsView.Scheme; }
|
||||
|
||||
LaunchScheme? IViewModelSupportLaunchExecution.CurrentScheme { get => Shared.GetCurrentLaunchSchemeFromConfigurationFile(); }
|
||||
@@ -123,9 +129,51 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
|
||||
|
||||
await HandleGamePathEntryChangeAsync().ConfigureAwait(false);
|
||||
Shared.ResumeLaunchExecutionAsync(this).SafeForget();
|
||||
|
||||
// 初始化第三方工具列表(不阻塞页面加载)
|
||||
_ = InitializeThirdPartyToolsInBackgroundAsync(token);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task InitializeThirdPartyToolsInBackgroundAsync(CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Yield to let navigation/UI finish first.
|
||||
await Task.Yield();
|
||||
|
||||
if (token.IsCancellationRequested || IsViewUnloaded.Value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImmutableArray<ToolInfo> tools = await InitializeThirdPartyToolsAsync(token).ConfigureAwait(false);
|
||||
|
||||
if (token.IsCancellationRequested || IsViewUnloaded.Value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
if (!token.IsCancellationRequested && !IsViewUnloaded.Value)
|
||||
{
|
||||
thirdPartyToolsField.Value = tools;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SentrySdk.AddBreadcrumb($"Failed to initialize third party tools: {ex.Message}", category: "ThirdPartyTool");
|
||||
SentrySdk.CaptureException(ex);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
[Command("IdentifyMonitorsCommand")]
|
||||
private static async Task IdentifyMonitorsAsync()
|
||||
{
|
||||
@@ -302,4 +350,47 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IView
|
||||
|
||||
await GameLifeCycle.TryKillGameProcessAsync(taskContext).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Command("ShowThirdPartyToolDialogCommand")]
|
||||
private async Task ShowThirdPartyToolDialogAsync(ToolInfo tool)
|
||||
{
|
||||
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateUI("Show third party tool dialog", "LaunchGameViewModel.Command"));
|
||||
|
||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||
{
|
||||
ThirdPartyToolDialog dialog = await scope.ServiceProvider
|
||||
.GetRequiredService<IContentDialogFactory>()
|
||||
.CreateInstanceAsync<ThirdPartyToolDialog>(scope.ServiceProvider, tool);
|
||||
|
||||
await dialog.ShowAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<ImmutableArray<ToolInfo>> InitializeThirdPartyToolsAsync(CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
SentrySdk.AddBreadcrumb("Starting to initialize third party tools", category: "ThirdPartyTool");
|
||||
IThirdPartyToolService thirdPartyToolService = serviceProvider.GetRequiredService<IThirdPartyToolService>();
|
||||
SentrySdk.AddBreadcrumb("Got IThirdPartyToolService instance", category: "ThirdPartyTool");
|
||||
|
||||
// Note: service API is not cancellable; we only honor cancellation before/after the call.
|
||||
token.ThrowIfCancellationRequested();
|
||||
ImmutableArray<ToolInfo> tools = await thirdPartyToolService.GetToolsAsync().ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
SentrySdk.AddBreadcrumb($"Got {tools.Length} tools from service", category: "ThirdPartyTool");
|
||||
return tools;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return ImmutableArray<ToolInfo>.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SentrySdk.AddBreadcrumb($"Failed to initialize third party tools: {ex.Message}", category: "ThirdPartyTool");
|
||||
SentrySdk.CaptureException(ex);
|
||||
return ImmutableArray<ToolInfo>.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,4 +16,6 @@ internal class UploadAnnouncement
|
||||
public string Link { get; set; } = default!;
|
||||
|
||||
public string? MaxPresentVersion { get; set; }
|
||||
|
||||
public string? Distribution { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Snap.Hutao.Web.ThirdPartyTool;
|
||||
|
||||
internal sealed class ToolApiResponse
|
||||
{
|
||||
[JsonPropertyName("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public ImmutableArray<ToolInfo> Data { get; set; } = ImmutableArray<ToolInfo>.Empty;
|
||||
}
|
||||
19
src/Snap.Hutao/Snap.Hutao/Web/ThirdPartyTool/ToolInfo.cs
Normal file
19
src/Snap.Hutao/Snap.Hutao/Web/ThirdPartyTool/ToolInfo.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Snap.Hutao.Web.ThirdPartyTool;
|
||||
|
||||
internal sealed class ToolInfo
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("desc")]
|
||||
public string Description { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("files")]
|
||||
public List<string> Files { get; set; } = default!;
|
||||
}
|
||||
Reference in New Issue
Block a user