6 Commits

Author SHA1 Message Date
fanbook-wangdage
2f148488f4 修复工具异步加载问题、添加武器和角色id、提示版本号 2026-01-16 12:33:26 +08:00
fanbook-wangdage
df92894307 支持公告中的发行版字段 2026-01-16 11:39:28 +08:00
fanbook-wangdage
5fad9ad855 提升版本号 2026-01-13 16:49:20 +08:00
fanbook-wangdage
1ed2f4f29e 支持注入时传命令行参数 2026-01-13 16:33:17 +08:00
fanbook-wangdage
db6df72791 添加using 2026-01-13 15:30:51 +08:00
fanbook-wangdage
bd9f188ac1 添加第三方工具功能 2026-01-13 15:17:20 +08:00
20 changed files with 634 additions and 16 deletions

Binary file not shown.

View File

@@ -2,7 +2,7 @@
<Package
Name="Snap.Hutao"
Manufacturer="Millennium Science Technology R-D Inst"
Version="1.17.4.0"
Version="1.18.2.0"
UpgradeCode="121203be-60cb-408f-92cc-7080f6598e68"
Scope="perMachine">

View File

@@ -39,6 +39,7 @@ internal static class DependencyInjection
.AddJsonOptions()
.AddDatabase()
.AddServices()
.AddThirdPartyToolService()
.AddResponseValidation()
.AddConfiguredHttpClients()

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@
<Identity
Name="60568DGPStudio.SnapHutao"
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
Version="1.18.0.0" />
Version="1.18.1.0" />
<Properties>
<DisplayName>Snap Hutao</DisplayName>

View File

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

View File

@@ -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,6 +155,7 @@ internal sealed class GameFpsUnlockInterop : IGameIslandInterop, IDisposable
RedirectStandardOutput = true,
RedirectStandardError = true,
WindowStyle = ProcessWindowStyle.Normal,
Arguments = gameArguments,
};
unlockerProcess = new Process { StartInfo = startInfo };
@@ -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)
{
// 恢复模式下,检查是否有解锁器进程在运行

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@
<UseWinUI>true</UseWinUI>
<UseWPF>False</UseWPF>
<!-- 配置版本号 -->
<Version>1.18.0.0</Version>
<Version>1.18.2.0</Version>
<UseWindowsForms>False</UseWindowsForms>
<ImplicitUsings>False</ImplicitUsings>

View File

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

View File

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

View File

@@ -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,6 +560,42 @@
Margin="16"
Padding="0"
cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<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"/>
@@ -923,6 +961,7 @@
</ContentControl>
</ScrollViewer>
</Grid>
</StackPanel>
</Border>
</PivotItem>
</Pivot>

View File

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

View File

@@ -16,4 +16,6 @@ internal class UploadAnnouncement
public string Link { get; set; } = default!;
public string? MaxPresentVersion { get; set; }
public string? Distribution { get; set; }
}

View File

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

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