1
0
mirror of https://github.com/wangdage12/Snap.Hutao.git synced 2026-03-28 19:05:16 +08:00

11 Commits

Author SHA1 Message Date
fanbook-wangdage
d3865b7992 Merge branch 'main' of https://github.com/wangdage12/Snap.Hutao 2026-03-24 20:19:13 +08:00
fanbook-wangdage
66263a82f2 更换新域名
支持打开debug控制台
修复一个可能导致元数据出问题的小问题
修改多处url
2026-03-24 20:03:45 +08:00
wangdage12
a2ddeadd0a Revise server status links and add sponsorship info
Updated server status page links and added sponsorship section.
2026-03-23 21:21:07 +08:00
wangdage12
246109227d Merge pull request #30 from wangdage12/dependabot/github_actions/dot-github/workflows/actions/upload-artifact-7
Bump actions/upload-artifact from 6 to 7 in /.github/workflows
2026-03-17 13:15:28 +08:00
dependabot[bot]
515fa004bb Bump actions/upload-artifact from 6 to 7 in /.github/workflows
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 02:57:32 +00:00
wangdage12
7c706f8dc7 Merge pull request #28 from wangdage12/dependabot/github_actions/dot-github/workflows/benc-uk/workflow-dispatch-1.3.1
Bump benc-uk/workflow-dispatch from 1.2.4 to 1.3.1 in /.github/workflows
2026-02-28 15:58:57 +08:00
fanbook-wangdage
9de07754c7 提升版本号 2026-02-28 15:25:31 +08:00
fanbook-wangdage
8e86ccc560 优化第三方工具功能(#25) 2026-02-28 15:19:16 +08:00
fanbook-wangdage
e1df07ac21 在没有本地化key时返回api的状态消息(fix #27) 2026-02-28 13:46:24 +08:00
dependabot[bot]
1dabff07a1 Bump benc-uk/workflow-dispatch from 1.2.4 to 1.3.1 in /.github/workflows
Bumps [benc-uk/workflow-dispatch](https://github.com/benc-uk/workflow-dispatch) from 1.2.4 to 1.3.1.
- [Release notes](https://github.com/benc-uk/workflow-dispatch/releases)
- [Commits](https://github.com/benc-uk/workflow-dispatch/compare/v1.2.4...v1.3.1)

---
updated-dependencies:
- dependency-name: benc-uk/workflow-dispatch
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 02:57:49 +00:00
fanbook-wangdage
640686a837 fix #26 2026-02-20 13:21:48 +08:00
36 changed files with 595 additions and 106 deletions

View File

@@ -64,7 +64,7 @@ jobs:
-Body $r2Data `
-ContentType "application/json"
Write-Output $response2.Content
- uses: benc-uk/workflow-dispatch@v1.2.4
- uses: benc-uk/workflow-dispatch@v1.3.1
with:
workflow: Build
repo: DGP-Studio/hutao-installer

View File

@@ -113,7 +113,7 @@ jobs:
- name: Upload signed msix
if: success() && github.event_name != 'pull_request'
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}
path: ${{ github.workspace }}/src/output/Snap.Hutao.Alpha-${{ steps.cake.outputs.version }}.msix

View File

@@ -77,7 +77,7 @@ jobs:
- name: Upload signed msix
if: ${{ success() && steps.merge.outputs.continue == 'true' }}
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: Snap.Hutao.Canary-${{ steps.cake.outputs.version }}
path: ${{ github.workspace }}/src/output/Snap.Hutao.Canary-${{ steps.cake.outputs.version }}.msix

View File

@@ -31,7 +31,7 @@ jobs:
run: dotnet build src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj -c Release
- name: Upload MSI Artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: Snap.Hutao-MSI
path: |

View File

@@ -6,7 +6,7 @@
自带的注入功能只有FPS调整只保证FPS调整长期可用你可以使用`注入选项`下方的第三方工具来使用更多功能,本项目提供的所有注入功能都不会影响游戏的公平性。
官网https://htserver.wdg.cloudns.ch/
官网https://htserver.wdg12.work/
**该版本的特点:**
- 尽量保留原版功能,少重写功能,稳定性强
@@ -42,7 +42,7 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
**目前元数据的编写进度:**
| 项目V6.3 | 是否完成 |
| 项目V6.4 | 是否完成 |
| ----------- | ----------- |
| 总体数据 | ✔️ |
@@ -67,6 +67,14 @@ https://deepwiki.com/DGP-Studio/Snap.Hutao.Server
- 服务端:[Snap.Server](https://github.com/wangdage12/Snap.Server)
- Web管理后台和官网[Snap.Server.Web](https://github.com/wangdage12/Snap.Server.Web)
**第三方工具**
如果你想要添加你自己开发的工具到第三方工具列表中,请确保:
1. 工具应该提供源码或者开源,并且可以成功编译
2. 工具不应提供任何可能影响游戏公平性的功能
工具不限于注入功能,若满足以上条件,请提 issue或者在 discord 服务器中联系管理员
## 打包测试
由于采用了 wix 进行打包程序VS 需要安装 **HeatWave for VS2022**2026兼容。需要 msi 安装包时,右键选中 Snap.Hutao.Installer 生成后即可在目标目录找到。默认目录Snap.Hutao.Installer\bin\x64\Release\en-US\Snap.Hutao.Installer.msi
@@ -85,9 +93,7 @@ https://deepwiki.com/DGP-Studio/Snap.Hutao.Server
我们将使用[UptimeRobot](https://uptimerobot.com)赞助的监控服务作为新的服务器状态页面,它有更多的功能
[服务器状态页面](https://stats.uptimerobot.com/fHxWxdxK61)
[旧服务器状态页面](http://serverjp.wdg.cloudns.ch:3001/status/hts)
[服务器状态页面](https://stats.uptimerobot.com/fHxWxdxK61)
---
@@ -95,7 +101,6 @@ https://deepwiki.com/DGP-Studio/Snap.Hutao.Server
https://github.com/wangdage12/Snap.Metadata
仓库镜像:
![http://serverjp.wdg.cloudns.ch:3001/api/badge/11/status?style=flat-square](http://serverjp.wdg.cloudns.ch:3001/api/badge/11/status?style=flat-square)
http://htgit.wdg.cloudns.ch/wdg1122/Snap.Metadata
@@ -105,10 +110,16 @@ http://htgit.wdg.cloudns.ch/wdg1122/Snap.Metadata
![http://serverjp.wdg.cloudns.ch:3001/api/badge/10/status?style=flat-square](http://serverjp.wdg.cloudns.ch:3001/api/badge/10/status?style=flat-square)
https://htserver.wdg.cloudns.ch/api/
https://htserver.wdg12.work/api/
---
**图片资源站:**
https://htserver.wdg.cloudns.ch/
https://htserver.wdg12.work/
# 赞助
如果你想要为我分摊经济压力,可以在下方链接中为我赞助(支持多个预设方案,你也可以在页面下方自定义金额)
赞助的资金将全部用于服务器、域名等若有剩余资金将升级CDN或者服务器来提升使用体验我们的服务是完全免费的该赞助并不会解锁额外特权但是>=10元时将在官网新的“赞助者页面”上添加你的信息
https://ifdian.net/a/wdg12

View File

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

View File

@@ -22,6 +22,11 @@ public static partial class Bootstrap
private static readonly ApplicationInitializationCallback AppInitializationCallback = InitializeApp;
private static Mutex? mutex;
/// <summary>
/// Gets a value indicating whether console output is enabled.
/// </summary>
public static bool ConsoleEnabled { get; private set; }
internal static void UseNamedPipeRedirection()
{
Debug.Assert(mutex is not null);
@@ -31,6 +36,19 @@ public static partial class Bootstrap
[STAThread]
private static void Main(string[] args)
{
// Check for console flag
ConsoleEnabled = args.Contains("--console") || args.Contains("--debug") ||
Environment.GetEnvironmentVariable("HUTAO_DEBUG") == "1";
if (ConsoleEnabled)
{
// Allocate a console window for debug output
HutaoNativeMethods.AllocConsole();
Console.WriteLine("[Bootstrap] Console allocated for debug output");
Console.WriteLine($"[Bootstrap] Arguments: {string.Join(" ", args)}");
Console.WriteLine($"[Bootstrap] Working Directory: {Environment.CurrentDirectory}");
}
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Starting...");
#endif
@@ -124,6 +142,12 @@ public static partial class Bootstrap
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Exiting");
#endif
if (ConsoleEnabled)
{
Console.WriteLine("[Bootstrap] Application exiting...");
HutaoNativeMethods.FreeConsole();
}
}
private static void InitializeApp(ApplicationInitializationCallbackParams param)

View File

@@ -29,6 +29,12 @@ internal static class DependencyInjection
.AddFilter(DbLoggerCategory.Query.Name, level => level >= LogLevel.Information)
.AddDebug()
.AddSentryTelemetry();
// Add console logging if console is enabled
if (Bootstrap.ConsoleEnabled)
{
builder.AddConsole();
}
})
.AddMemoryCache()

View File

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

View File

@@ -1127,12 +1127,21 @@
<data name="ServiceGameSetMultiChannelUnauthorizedAccess" xml:space="preserve">
<value>无法读取或保存配置文件,请以管理员模式重试</value>
</data>
<data name="ServiceGitRepositoryCloneReasonForceInvalid" xml:space="preserve">
<value>现有元数据仓库更新失败,正在重新下载</value>
</data>
<data name="ServiceGitRepositoryCloneReasonInvalidRepo" xml:space="preserve">
<value>首次下载或仓库损坏,正在下载元数据</value>
</data>
<data name="ServiceGitRepositoryOperationCompleted" xml:space="preserve">
<value>操作完成</value>
</data>
<data name="ServiceGitRepositoryOperationFailed" xml:space="preserve">
<value>操作失败</value>
</data>
<data name="ServiceGitRepositoryUpdatingExisting" xml:space="preserve">
<value>检查元数据更新</value>
</data>
<data name="ServiceHutaoUserGachaLogExpiredAt" xml:space="preserve">
<value>祈愿记录上传服务有效期至</value>
</data>
@@ -1598,6 +1607,9 @@
<data name="ViewDialogThirdPartyToolLaunch" xml:space="preserve">
<value>启动</value>
</data>
<data name="ViewDialogThirdPartyToolVersion" xml:space="preserve">
<value>版本:</value>
</data>
<data name="ViewDialogQRCodeTitle" xml:space="preserve">
<value>使用米游社扫描二维码</value>
</data>

View File

@@ -38,6 +38,15 @@ internal sealed partial class GachaLogService : IGachaLogService
}
}
public async ValueTask<IAdvancedDbCollectionView<GachaArchive>> RefreshArchiveCollectionAsync()
{
using (await archivesLock.LockAsync().ConfigureAwait(false))
{
archives = null;
return archives = gachaLogRepository.GetGachaArchiveCollection().ToAdvancedDbCollectionView(serviceProvider);
}
}
public async ValueTask<GachaStatistics> GetStatisticsAsync(GachaLogServiceMetadataContext context, GachaArchive archive)
{
using (ValueStopwatch.MeasureExecution(logger))

View File

@@ -22,4 +22,6 @@ internal interface IGachaLogService
ValueTask RemoveArchiveAsync(GachaArchive archive);
ValueTask<IAdvancedDbCollectionView<GachaArchive>> GetArchiveCollectionAsync();
ValueTask<IAdvancedDbCollectionView<GachaArchive>> RefreshArchiveCollectionAsync();
}

View File

@@ -21,6 +21,7 @@ internal sealed partial class GitRepositoryService : IGitRepositoryService
{
private readonly AsyncKeyedLock<string> repoLock = new();
private readonly BackgroundActivityOptions backgroundActivityOptions;
private readonly ILogger<GitRepositoryService> logger;
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
@@ -76,6 +77,7 @@ internal sealed partial class GitRepositoryService : IGitRepositoryService
}
catch (Exception first)
{
logger.LogWarning(first, "[Metadata] Failed to update existing repository, fallback to reclone: Directory={Directory}, Url={Url}", directory, info.HttpsUrl.OriginalString);
exceptions.Add(first);
return EnsureRepository(activity, directory, info, true);
}
@@ -110,6 +112,14 @@ internal sealed partial class GitRepositoryService : IGitRepositoryService
{
// Increase & decrease count in the same method, so that crash in the middle can correctly count as failure.
RepositoryAffinity.IncreaseFailure(info);
// Debug: Log the initial state
bool isRepoValid = Repository.IsValid(directory);
bool directoryExists = Directory.Exists(directory);
logger.LogInformation("[Metadata] Checking repository: Directory={Directory}, Exists={Exists}, IsValid={IsValid}, ForceInvalid={ForceInvalid}",
directory, directoryExists, isRepoValid, forceInvalid);
FetchOptions fetchOptions = new()
{
Depth = 1,
@@ -142,10 +152,18 @@ internal sealed partial class GitRepositoryService : IGitRepositoryService
CertificateCheck = static (cert, valid, host) => true,
};
if (forceInvalid || !Repository.IsValid(directory))
if (forceInvalid || !isRepoValid)
{
if (Directory.Exists(directory))
// Debug: Log why we're cloning
string reason = forceInvalid
? SH.ServiceGitRepositoryCloneReasonForceInvalid
: SH.ServiceGitRepositoryCloneReasonInvalidRepo;
logger.LogInformation("[Metadata] Cloning repository: Reason={Reason}, Url={Url}", reason, info.HttpsUrl.OriginalString);
activity.Update(taskContext, reason, false, false, false, false);
if (directoryExists)
{
logger.LogInformation("[Metadata] Deleting existing directory before clone");
Directory.SetReadOnly(directory, false);
Directory.Delete(directory, true);
}
@@ -154,9 +172,15 @@ internal sealed partial class GitRepositoryService : IGitRepositoryService
{
Checkout = true,
});
logger.LogInformation("[Metadata] Clone completed successfully");
}
else
{
// Debug: Log that we're updating
logger.LogInformation("[Metadata] Updating existing repository");
activity.Update(taskContext, SH.ServiceGitRepositoryUpdatingExisting, false, false, false, false);
// We need to ensure local repo is up to date
using (Repository repo = new(directory))
{
@@ -177,17 +201,20 @@ internal sealed partial class GitRepositoryService : IGitRepositoryService
repo.Network.Remotes.Update("origin", remote => remote.Url = info.HttpsUrl.OriginalString);
repo.RemoveUntrackedFiles();
fetchOptions.UpdateFetchHead = false;
Commands.Fetch(repo, repo.Head.RemoteName, Array.Empty<string>(), fetchOptions, default);
Commands.Fetch(repo, "origin", Array.Empty<string>(), fetchOptions, default);
// Manually patch .git/shallow file
File.WriteAllText(Path.Combine(directory, ".git//shallow"), string.Join("", repo.Branches.Where(static branch => branch.IsRemote).Select(static branch => $"{branch.Tip.Sha}\n")));
File.WriteAllText(Path.Combine(directory, ".git", "shallow"), string.Join("", repo.Branches.Where(static branch => branch.IsRemote).Select(static branch => $"{branch.Tip.Sha}\n")));
Branch remoteBranch = repo.Branches["origin/main"];
Branch localBranch = repo.Branches["main"] ?? repo.CreateBranch("main", remoteBranch.Tip);
repo.Branches.Update(localBranch, b => b.TrackedBranch = remoteBranch.CanonicalName);
Commands.Checkout(repo, localBranch);
repo.Reset(ResetMode.Hard, remoteBranch.Tip);
repo.RemoveUntrackedFiles();
}
logger.LogInformation("[Metadata] Update completed successfully");
}
RepositoryAffinity.DecreaseFailure(info);

View File

@@ -34,4 +34,18 @@ internal interface IThirdPartyToolService
/// <param name="tool">工具信息</param>
/// <returns>是否已下载</returns>
bool IsToolDownloaded(ToolInfo tool);
/// <summary>
/// 获取本地工具信息
/// </summary>
/// <param name="tool">工具信息</param>
/// <returns>本地工具信息如果不存在则返回null</returns>
LocalToolInfo? GetLocalToolInfo(ToolInfo tool);
/// <summary>
/// 检查工具是否需要更新
/// </summary>
/// <param name="tool">工具信息</param>
/// <returns>是否需要更新</returns>
bool NeedsUpdate(ToolInfo tool);
}

View File

@@ -9,7 +9,9 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Text.Json;
namespace Snap.Hutao.Service.ThirdPartyTool;
@@ -17,8 +19,9 @@ namespace Snap.Hutao.Service.ThirdPartyTool;
[Service(ServiceLifetime.Singleton, typeof(IThirdPartyToolService))]
internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
{
private const string ApiBaseUrl = "https://htserver.wdg.cloudns.ch/api";
private const string ApiBaseUrl = "https://htserver.wdg12.work/api";
private const string ToolsEndpoint = "/tools";
private const string ToolInfoFileName = "tool_info.json";
private readonly IHttpClientFactory httpClientFactory;
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
@@ -32,10 +35,10 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
try
{
HttpClient httpClient = httpClientFactory.CreateClient();
// 添加日志
SentrySdk.AddBreadcrumb($"Creating request to: {ApiBaseUrl}{ToolsEndpoint}", category: "ThirdPartyTool");
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri($"{ApiBaseUrl}{ToolsEndpoint}")
.Get();
@@ -89,41 +92,32 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
try
{
string toolDirectory = GetToolDirectory(tool);
Directory.CreateDirectory(toolDirectory);
int totalFiles = tool.Files.Count;
int downloadedFiles = 0;
// 如果需要更新,先清理旧文件
if (NeedsUpdate(tool) && Directory.Exists(toolDirectory))
{
Directory.Delete(toolDirectory, true);
}
Directory.CreateDirectory(toolDirectory);
using (HttpClient httpClient = httpClientFactory.CreateClient())
{
foreach (string fileName in tool.Files)
if (tool.IsCompressed)
{
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);
// 压缩包模式:下载并解压
await DownloadAndExtractCompressedToolAsync(httpClient, tool, toolDirectory, progress, token).ConfigureAwait(false);
}
else
{
// 非压缩包模式:直接下载所有文件
await DownloadFilesAsync(httpClient, tool, toolDirectory, progress, token).ConfigureAwait(false);
}
}
// 保存本地工具信息
SaveLocalToolInfo(tool);
return true;
}
catch (Exception ex)
@@ -139,15 +133,31 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
{
string toolDirectory = GetToolDirectory(tool);
// 查找可执行文件.exe
string? executablePath = tool.Files.FirstOrDefault(f => f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase));
// 优先使用 main_exe如果没有则查找可执行文件
string? executablePath = tool.MainExe;
if (string.IsNullOrEmpty(executablePath))
{
executablePath = tool.Files.FirstOrDefault(f => f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase));
}
// 如果还是没有,尝试从目录中查找
if (string.IsNullOrEmpty(executablePath))
{
string[] exeFiles = Directory.GetFiles(toolDirectory, "*.exe", SearchOption.TopDirectoryOnly);
executablePath = exeFiles.FirstOrDefault();
}
if (string.IsNullOrEmpty(executablePath))
{
messenger.Send(InfoBarMessage.Warning(SH.ServiceThirdPartyToolNoExecutableFound));
return false;
}
string fullPath = Path.Combine(toolDirectory, executablePath);
// 如果 executablePath 是完整路径,直接使用;否则拼接目录
string fullPath = Path.IsPathRooted(executablePath)
? executablePath
: Path.Combine(toolDirectory, Path.GetFileName(executablePath));
if (!File.Exists(fullPath))
{
messenger.Send(InfoBarMessage.Warning(SH.FormatServiceThirdPartyToolFileNotFound(fullPath)));
@@ -192,7 +202,20 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
return false;
}
// 检查所有文件是否存在
// 检查工具信息文件是否存在
LocalToolInfo? localInfo = GetLocalToolInfo(tool);
if (localInfo is null)
{
return false;
}
// 对于压缩包,检查目录是否有内容
if (tool.IsCompressed)
{
return Directory.GetFiles(toolDirectory, "*", SearchOption.AllDirectories).Length > 0;
}
// 对于非压缩包,检查所有文件是否存在
foreach (string fileName in tool.Files)
{
string filePath = Path.Combine(toolDirectory, fileName);
@@ -205,6 +228,217 @@ internal sealed partial class ThirdPartyToolService : IThirdPartyToolService
return true;
}
public LocalToolInfo? GetLocalToolInfo(ToolInfo tool)
{
string toolDirectory = GetToolDirectory(tool);
string infoFilePath = Path.Combine(toolDirectory, ToolInfoFileName);
if (!File.Exists(infoFilePath))
{
return null;
}
try
{
string json = File.ReadAllText(infoFilePath);
return JsonSerializer.Deserialize<LocalToolInfo>(json);
}
catch
{
return null;
}
}
public bool NeedsUpdate(ToolInfo tool)
{
LocalToolInfo? localInfo = GetLocalToolInfo(tool);
if (localInfo is null)
{
return true; // 没有本地信息,需要下载
}
// 比较版本号
return IsNewerVersion(tool.Version, localInfo.Version);
}
private static async Task DownloadAndExtractCompressedToolAsync(
HttpClient httpClient,
ToolInfo tool,
string toolDirectory,
IProgress<double>? progress,
CancellationToken token)
{
// 压缩包模式通常只有一个 zip 文件
string zipFileName = tool.Files.FirstOrDefault(f => f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
?? tool.Files[0];
string zipUrl = $"{tool.Url}{zipFileName}";
string zipFilePath = Path.Combine(toolDirectory, zipFileName);
// 下载 zip 文件
progress?.Report(0);
HttpResponseMessage response = await httpClient.GetAsync(zipUrl, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
using (Stream contentStream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false))
using (FileStream fileStream = new(zipFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
{
await contentStream.CopyToAsync(fileStream, token).ConfigureAwait(false);
}
progress?.Report(50);
// 解压 zip 文件
using (ZipArchive archive = ZipFile.OpenRead(zipFilePath))
{
// 检查是否有根目录需要处理
bool hasRootFolder = HasSingleRootFolder(archive);
foreach (ZipArchiveEntry entry in archive.Entries)
{
if (string.IsNullOrEmpty(entry.Name))
{
// 这是一个目录,创建它
string? destinationPath = GetDestinationPath(entry.FullName, toolDirectory, hasRootFolder);
if (destinationPath is not null)
{
Directory.CreateDirectory(destinationPath);
}
continue;
}
string? destFilePath = GetDestinationPath(entry.FullName, toolDirectory, hasRootFolder);
if (destFilePath is null)
{
continue;
}
// 确保目录存在
string? destDir = Path.GetDirectoryName(destFilePath);
if (!string.IsNullOrEmpty(destDir))
{
Directory.CreateDirectory(destDir);
}
entry.ExtractToFile(destFilePath, true);
}
}
progress?.Report(90);
// 删除 zip 文件
File.Delete(zipFilePath);
progress?.Report(100);
}
private static async Task DownloadFilesAsync(
HttpClient httpClient,
ToolInfo tool,
string toolDirectory,
IProgress<double>? progress,
CancellationToken token)
{
int totalFiles = tool.Files.Count;
int downloadedFiles = 0;
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);
}
}
private void SaveLocalToolInfo(ToolInfo tool)
{
string toolDirectory = GetToolDirectory(tool);
string infoFilePath = Path.Combine(toolDirectory, ToolInfoFileName);
LocalToolInfo localInfo = LocalToolInfo.FromToolInfo(tool);
string json = JsonSerializer.Serialize(localInfo, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(infoFilePath, json);
}
private static bool IsNewerVersion(string remoteVersion, string localVersion)
{
// 使用 Version 类进行比较
if (Version.TryParse(remoteVersion, out Version? remote) && Version.TryParse(localVersion, out Version? local))
{
return remote > local;
}
// 如果无法解析为版本号,进行字符串比较
return !string.Equals(remoteVersion, localVersion, StringComparison.OrdinalIgnoreCase);
}
private static bool HasSingleRootFolder(ZipArchive archive)
{
// 检查是否所有条目都在同一个根目录下
HashSet<string> rootFolders = [];
foreach (ZipArchiveEntry entry in archive.Entries)
{
int separatorIndex = entry.FullName.IndexOf('/');
if (separatorIndex > 0)
{
rootFolders.Add(entry.FullName[..separatorIndex]);
}
else if (separatorIndex < 0 && !string.IsNullOrEmpty(entry.Name))
{
// 直接在根目录下的文件
return false;
}
}
return rootFolders.Count == 1;
}
private static string? GetDestinationPath(string entryPath, string toolDirectory, bool hasRootFolder)
{
if (string.IsNullOrEmpty(entryPath))
{
return null;
}
if (hasRootFolder)
{
// 移除根目录前缀
int separatorIndex = entryPath.IndexOf('/');
if (separatorIndex >= 0 && separatorIndex < entryPath.Length - 1)
{
return Path.Combine(toolDirectory, entryPath[(separatorIndex + 1)..]);
}
else if (separatorIndex >= 0)
{
// 这是根目录本身
return null;
}
}
return Path.Combine(toolDirectory, entryPath);
}
private static string GetToolDirectory(ToolInfo tool)
{
// 使用数据目录/工具名作为存储路径

View File

@@ -4,6 +4,7 @@ using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.InterChange.GachaLog;
using Snap.Hutao.Service.GachaLog;
using Snap.Hutao.ViewModel.GachaLog;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using System.Collections.Immutable;
@@ -13,6 +14,7 @@ internal abstract partial class AbstractUIGF40ImportService : IUIGFImportService
{
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private readonly IMessenger messenger;
[GeneratedConstructor]
public partial AbstractUIGF40ImportService(IServiceProvider serviceProvider);
@@ -21,6 +23,7 @@ internal abstract partial class AbstractUIGF40ImportService : IUIGFImportService
{
await taskContext.SwitchToBackgroundAsync();
ImportGachaArchives(importOptions.UIGF.Hk4e, importOptions.GachaArchiveUids);
messenger.Send(GachaLogImportedMessage.Empty);
}
private void ImportGachaArchives(ImmutableArray<UIGFEntry<Hk4eItem>> entries, HashSet<uint> uids)

View File

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

View File

@@ -117,10 +117,17 @@ internal partial class ScopedPage : Page
if (DataContext is IViewModel viewModel)
{
// Wait to ensure critical viewmodel operation is completed
using (viewModel.CriticalSection.Enter())
try
{
// Wait to ensure critical viewmodel operation is completed.
// The view model might already be disposed when window shutdown and page unload race.
using (viewModel.CriticalSection.Enter())
{
viewModel.Uninitialize();
}
}
catch (OperationCanceledException)
{
viewModel.Uninitialize();
}
try

View File

@@ -1,7 +1,7 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- DocumentLink -->
<x:String x:Key="DocumentLink_BugReport">https://hut.ao/statements/bug-report.html</x:String>
<x:String x:Key="DocumentLink_Home">https://hut.ao</x:String>
<x:String x:Key="DocumentLink_Home">https://htserver.wdg12.work</x:String>
<x:String x:Key="DocumentLink_MhyAccountSwitch">https://hut.ao/features/mhy-account-switch.html</x:String>
<x:String x:Key="DocumentLink_Translate">https://translate.hut.ao</x:String>
<x:String x:Key="DocumentLink_Loopback">https://hut.ao/advanced/loopback.html</x:String>
@@ -13,32 +13,32 @@
<x:String x:Key="IconGame">https://launcher-webstatic.mihoyo.com/launcher-public/2024/04/15/9ebf1bc5af2d83ca5fca21adb49cf341_2571779162329842818.png</x:String>
<!-- AvatarCard -->
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://htserver.wdg.cloudns.ch/static/raw/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
<x:String x:Key="UI_AvatarIcon_Costume_Card">https://htserver.wdg12.work/static/raw/AvatarCard/UI_AvatarIcon_Costume_Card.png</x:String>
<!-- Bg -->
<x:String x:Key="UI_ItemIcon_None">https://htserver.wdg.cloudns.ch/static/raw/Bg/UI_ItemIcon_None.png</x:String>
<x:String x:Key="UI_ItemIcon_None">https://htserver.wdg12.work/static/raw/Bg/UI_ItemIcon_None.png</x:String>
<!-- Mark -->
<x:String x:Key="UI_MarkQuest_Events_Proce">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkQuest_Events_Proce.png</x:String>
<x:String x:Key="UI_MarkQuest_Events_Start">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkQuest_Events_Start.png</x:String>
<x:String x:Key="UI_MarkQuest_Main_Proce">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkQuest_Main_Proce.png</x:String>
<x:String x:Key="UI_MarkQuest_Main_Start">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkQuest_Main_Start.png</x:String>
<x:String x:Key="UI_MarkTower">https://htserver.wdg.cloudns.ch/static/raw/Mark/UI_MarkTower.png</x:String>
<x:String x:Key="UI_MarkQuest_Events_Proce">https://htserver.wdg12.work/static/raw/Mark/UI_MarkQuest_Events_Proce.png</x:String>
<x:String x:Key="UI_MarkQuest_Events_Start">https://htserver.wdg12.work/static/raw/Mark/UI_MarkQuest_Events_Start.png</x:String>
<x:String x:Key="UI_MarkQuest_Main_Proce">https://htserver.wdg12.work/static/raw/Mark/UI_MarkQuest_Main_Proce.png</x:String>
<x:String x:Key="UI_MarkQuest_Main_Start">https://htserver.wdg12.work/static/raw/Mark/UI_MarkQuest_Main_Start.png</x:String>
<x:String x:Key="UI_MarkTower">https://htserver.wdg12.work/static/raw/Mark/UI_MarkTower.png</x:String>
<!-- ItemIcon -->
<x:String x:Key="UI_ItemIcon_106">https://htserver.wdg.cloudns.ch/static/raw/ItemIcon/UI_ItemIcon_106.png</x:String>
<x:String x:Key="UI_ItemIcon_204">https://htserver.wdg.cloudns.ch/static/raw/ItemIcon/UI_ItemIcon_204.png</x:String>
<x:String x:Key="UI_ItemIcon_210">https://htserver.wdg.cloudns.ch/static/raw/ItemIcon/UI_ItemIcon_210.png</x:String>
<x:String x:Key="UI_ItemIcon_220021">https://htserver.wdg.cloudns.ch/static/raw/ItemIcon/UI_ItemIcon_220021.png</x:String>
<x:String x:Key="UI_ItemIcon_106">https://htserver.wdg12.work/static/raw/ItemIcon/UI_ItemIcon_106.png</x:String>
<x:String x:Key="UI_ItemIcon_204">https://htserver.wdg12.work/static/raw/ItemIcon/UI_ItemIcon_204.png</x:String>
<x:String x:Key="UI_ItemIcon_210">https://htserver.wdg12.work/static/raw/ItemIcon/UI_ItemIcon_210.png</x:String>
<x:String x:Key="UI_ItemIcon_220021">https://htserver.wdg12.work/static/raw/ItemIcon/UI_ItemIcon_220021.png</x:String>
<!-- EmotionIcon -->
<x:String x:Key="UI_EmotionIcon52">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon52.png</x:String>
<x:String x:Key="UI_EmotionIcon71">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
<x:String x:Key="UI_EmotionIcon89">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon89.png</x:String>
<x:String x:Key="UI_EmotionIcon250">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon271">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
<x:String x:Key="UI_EmotionIcon272">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
<x:String x:Key="UI_EmotionIcon293">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
<x:String x:Key="UI_EmotionIcon433">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon433.png</x:String>
<x:String x:Key="UI_EmotionIcon445">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
<x:String x:Key="UI_EmotionIcon585">https://htserver.wdg.cloudns.ch/static/raw/EmotionIcon/UI_EmotionIcon585.png</x:String>
<x:String x:Key="UI_EmotionIcon52">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon52.png</x:String>
<x:String x:Key="UI_EmotionIcon71">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
<x:String x:Key="UI_EmotionIcon89">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon89.png</x:String>
<x:String x:Key="UI_EmotionIcon250">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon271">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
<x:String x:Key="UI_EmotionIcon272">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
<x:String x:Key="UI_EmotionIcon293">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
<x:String x:Key="UI_EmotionIcon433">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon433.png</x:String>
<x:String x:Key="UI_EmotionIcon445">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
<x:String x:Key="UI_EmotionIcon585">https://htserver.wdg12.work/static/raw/EmotionIcon/UI_EmotionIcon585.png</x:String>
</ResourceDictionary>

View File

@@ -57,7 +57,7 @@ internal sealed partial class HutaoPassportRegisterDialog : ContentDialog
return;
}
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessage()));
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessageOrMessage()));
}
}
}

View File

@@ -58,7 +58,7 @@ internal sealed partial class HutaoPassportResetPasswordDialog : ContentDialog
return;
}
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessage()));
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessageOrMessage()));
}
}

View File

@@ -70,7 +70,7 @@ internal sealed partial class HutaoPassportResetUsernameDialog : ContentDialog
return;
}
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessage()));
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessageOrMessage()));
}
}

View File

@@ -60,7 +60,7 @@ internal sealed partial class HutaoPassportUnregisterDialog : ContentDialog
return;
}
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessage()));
messenger.Send(InfoBarMessage.Information(response.GetLocalizationMessageOrMessage()));
}
}

View File

@@ -17,6 +17,7 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock
@@ -31,8 +32,17 @@
Text="{x:Bind Tool.Description, Mode=OneWay}"
TextWrapping="Wrap"/>
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="8">
<TextBlock
Style="{StaticResource BodyTextBlockStyle}"
Text="{shuxm:ResourceString Name=ViewDialogThirdPartyToolVersion}"/>
<TextBlock
Style="{StaticResource BodyStrongTextBlockStyle}"
Text="{x:Bind Tool.Version, Mode=OneWay}"/>
</StackPanel>
<ProgressBar
Grid.Row="2"
Grid.Row="3"
Height="4"
IsIndeterminate="{x:Bind IsDownloading.Value, Mode=OneWay, FallbackValue=False}"
Visibility="{x:Bind IsDownloading.Value, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"/>

View File

@@ -34,13 +34,20 @@ internal sealed partial class ThirdPartyToolDialog : ContentDialog
{
// 在 UI 线程上获取 Tool 的引用,避免后续跨线程访问依赖属性
ToolInfo? tool = Tool;
try
{
IsDownloading = true;
// 检查工具是否已下载
if (tool is not null && !thirdPartyToolService.IsToolDownloaded(tool))
if (tool is null)
{
return;
}
// 检查工具是否需要下载或更新
bool needDownload = !thirdPartyToolService.IsToolDownloaded(tool) || thirdPartyToolService.NeedsUpdate(tool);
if (needDownload)
{
// 下载工具
bool downloadSuccess = await thirdPartyToolService.DownloadToolAsync(tool, null).ConfigureAwait(false);
@@ -53,15 +60,12 @@ internal sealed partial class ThirdPartyToolDialog : ContentDialog
}
// 启动工具
if (tool is not null)
bool launchSuccess = await thirdPartyToolService.LaunchToolAsync(tool).ConfigureAwait(false);
if (launchSuccess)
{
bool launchSuccess = await thirdPartyToolService.LaunchToolAsync(tool).ConfigureAwait(false);
if (launchSuccess)
{
await contentDialogFactory.TaskContext.SwitchToMainThreadAsync();
Hide();
return;
}
await contentDialogFactory.TaskContext.SwitchToMainThreadAsync();
Hide();
return;
}
}
catch (Exception ex)

View File

@@ -268,7 +268,7 @@
<TextBlock>
<TextBlock.Inlines>
<Run Text="{shuxm:ResourceString Name=ViewGuideStepAgreementIHaveReadText}"/>
<Hyperlink NavigateUri="https://github.com/DGP-Studio/Snap.Hutao/blob/main/LICENSE">
<Hyperlink NavigateUri="https://github.com/wangdage12/Snap.Hutao/blob/main/LICENSE">
<Run Text="{shuxm:ResourceString Name=ViewGuideStepAgreementOpenSourceLicense}"/>
</Hyperlink>
</TextBlock.Inlines>

View File

@@ -93,13 +93,13 @@
<cwcont:SettingsExpander.Items>
<cwcont:SettingsCard
Command="{Binding NavigateToUriCommand}"
CommandParameter="https://github.com/DGP-Studio/Snap.Hutao/issues/new/choose"
CommandParameter="https://github.com/wangdage12/Snap.Hutao"
Description="{shuxm:ResourceString Name=ViewPageFeedbackGithubIssuesDescription}"
Header="GitHub Issues"
IsClickEnabled="True"/>
<cwcont:SettingsCard
Command="{Binding NavigateToUriCommand}"
CommandParameter="https://status.snapgenshin.cn/status"
CommandParameter="https://stats.uptimerobot.com/fHxWxdxK61"
Description="{shuxm:ResourceString Name=ViewPageFeedbackServerStatusDescription}"
Header="{shuxm:ResourceString Name=ViewPageFeedbackServerStatusHeader}"
IsClickEnabled="True"/>

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.ViewModel.GachaLog;
internal sealed class GachaLogImportedMessage
{
public static readonly GachaLogImportedMessage Empty = new();
private GachaLogImportedMessage()
{
}
}

View File

@@ -26,7 +26,7 @@ namespace Snap.Hutao.ViewModel.GachaLog;
[BindableCustomPropertyProvider]
[Service(ServiceLifetime.Scoped)]
internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
internal sealed partial class GachaLogViewModel : Abstraction.ViewModel, IRecipient<GachaLogImportedMessage>
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly IServiceProvider serviceProvider;
@@ -127,6 +127,22 @@ internal sealed partial class GachaLogViewModel : Abstraction.ViewModel
UpdateStatisticsAsync(Archives?.CurrentItem).SafeForget();
}
public async void Receive(GachaLogImportedMessage message)
{
await RefreshArchiveCollectionAsync().ConfigureAwait(false);
}
private async ValueTask RefreshArchiveCollectionAsync()
{
using (await EnterCriticalSectionAsync().ConfigureAwait(false))
{
IAdvancedDbCollectionView<GachaArchive> archives = await gachaLogService.RefreshArchiveCollectionAsync().ConfigureAwait(false);
await taskContext.SwitchToMainThreadAsync();
Archives = archives;
Archives.MoveCurrentTo(Archives.Source.SelectedOrFirstOrDefault());
}
}
[Command("RefreshByWebCacheCommand")]
private async Task RefreshByWebCacheAsync()
{

View File

@@ -193,11 +193,11 @@ internal sealed partial class SpiralAbyssViewModel : Abstraction.ViewModel, IRec
{
HutaoResponse response = await spiralAbyssClient.UploadRecordAsync(record).ConfigureAwait(false);
if (response is ILocalizableResponse localizableResponse)
if (response is ILocalizableResponse)
{
messenger.Send(InfoBarMessage.Any(
response is { ReturnCode: 0 } ? InfoBarSeverity.Success : InfoBarSeverity.Warning,
localizableResponse.GetLocalizationMessage()));
response.GetLocalizationMessageOrMessage()));
}
}

View File

@@ -6,9 +6,9 @@ namespace Snap.Hutao.Web.Endpoint.Hutao;
[Service(ServiceLifetime.Singleton, typeof(IHutaoEndpoints), Key = HutaoEndpointsKind.Release)]
internal sealed class HutaoEndpointsForRelease : IHutaoEndpoints
{
string IHomaRootAccess.Root { get => "https://htserver.wdg.cloudns.ch/api"; }
string IHomaRootAccess.Root { get => "https://htserver.wdg12.work/api"; }
string IInfrastructureRootAccess.Root { get => "https://htserver.wdg.cloudns.ch/api"; }
string IInfrastructureRootAccess.Root { get => "https://htserver.wdg12.work/api"; }
string IInfrastructureRawRootAccess.RawRoot { get => "https://htserver.wdg.cloudns.ch/api"; }
string IInfrastructureRawRootAccess.RawRoot { get => "https://htserver.wdg12.work/api"; }
}

View File

@@ -5,7 +5,7 @@ namespace Snap.Hutao.Web.Endpoint.Hutao;
internal static class StaticResourcesEndpoints
{
public static string Root { get => "https://htserver.wdg.cloudns.ch"; }
public static string Root { get => "https://htserver.wdg12.work"; }
public static Uri UIIconNone { get; } = StaticRaw("Bg", "UI_Icon_None.png").ToUri();

View File

@@ -24,7 +24,7 @@ internal sealed class HutaoResponse : Web.Response.Response, ILocalizableRespons
public override string ToString()
{
return SH.FormatWebResponse(ReturnCode, this.GetLocalizationMessageOrDefault());
return SH.FormatWebResponse(ReturnCode, this.GetLocalizationMessageOrMessage());
}
}
@@ -48,6 +48,6 @@ internal sealed class HutaoResponse<TData> : Response<TData>, ILocalizableRespon
public override string ToString()
{
return SH.FormatWebResponse(ReturnCode, this.GetLocalizationMessageOrDefault());
return SH.FormatWebResponse(ReturnCode, this.GetLocalizationMessageOrMessage());
}
}

View File

@@ -0,0 +1,54 @@
using System.Text.Json.Serialization;
namespace Snap.Hutao.Web.ThirdPartyTool;
/// <summary>
/// 本地工具信息,用于保存工具的本地状态
/// </summary>
internal sealed class LocalToolInfo
{
/// <summary>
/// 工具名称
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = default!;
/// <summary>
/// 工具版本号
/// </summary>
[JsonPropertyName("version")]
public string Version { get; set; } = default!;
/// <summary>
/// 是否为压缩包
/// </summary>
[JsonPropertyName("is_compressed")]
public bool IsCompressed { get; set; }
/// <summary>
/// 主可执行文件名
/// </summary>
[JsonPropertyName("main_exe")]
public string? MainExe { get; set; }
/// <summary>
/// 下载时间
/// </summary>
[JsonPropertyName("download_time")]
public DateTimeOffset DownloadTime { get; set; }
/// <summary>
/// 从 ToolInfo 创建 LocalToolInfo
/// </summary>
public static LocalToolInfo FromToolInfo(ToolInfo toolInfo)
{
return new LocalToolInfo
{
Name = toolInfo.Name,
Version = toolInfo.Version,
IsCompressed = toolInfo.IsCompressed,
MainExe = toolInfo.MainExe,
DownloadTime = DateTimeOffset.Now,
};
}
}

View File

@@ -16,4 +16,13 @@ internal sealed class ToolInfo
[JsonPropertyName("files")]
public List<string> Files { get; set; } = default!;
[JsonPropertyName("is_compressed")]
public bool IsCompressed { get; set; }
[JsonPropertyName("main_exe")]
public string? MainExe { get; set; }
[JsonPropertyName("version")]
public string Version { get; set; } = default!;
}

View File

@@ -42,6 +42,10 @@ internal static unsafe class HutaoNativeMethods
// ReSharper restore InconsistentNaming
public const string DllName = "Snap.Hutao.Native.dll";
// Console APIs
public const int STD_OUTPUT_HANDLE = -11;
public const int STD_ERROR_HANDLE = -12;
public static HutaoNative HutaoCreateInstance()
{
nint pv = default;
@@ -54,6 +58,36 @@ internal static unsafe class HutaoNativeMethods
return HutaoHResultIsWin32(hr, error);
}
/// <summary>
/// Allocates a new console for the calling process.
/// </summary>
/// <returns>TRUE if the function succeeds; otherwise, FALSE.</returns>
[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern BOOL AllocConsole();
/// <summary>
/// Detaches the calling process from its console.
/// </summary>
/// <returns>TRUE if the function succeeds; otherwise, FALSE.</returns>
[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern BOOL FreeConsole();
/// <summary>
/// Attaches the calling process to the console of the specified process.
/// </summary>
/// <param name="dwProcessId">The identifier of the process whose console is to be used.</param>
/// <returns>TRUE if the function succeeds; otherwise, FALSE.</returns>
[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern BOOL AttachConsole(uint dwProcessId);
/// <summary>
/// Retrieves a handle to the specified standard device.
/// </summary>
/// <param name="nStdHandle">The standard device.</param>
/// <returns>A handle to the specified device.</returns>
[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern nint GetStdHandle(int nStdHandle);
[DllImport(DllName, CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]
private static extern HRESULT HutaoCreateInstance(HutaoNative.Vftbl** ppv);