Compare commits

..

12 Commits

Author SHA1 Message Date
DismissedLight
6ee823094a Merge pull request #1207 from DGP-Studio/develop 2023-12-23 11:51:15 +08:00
qhy040404
14894b0b47 Update issue templates 2023-12-22 16:59:22 +08:00
Masterain
6834073603 Merge pull request #1204 from DGP-Studio/issue_template_publish
Update MGMT-publish.yml
2023-12-21 17:41:22 -08:00
qhy040404
911fe57fb2 check jihulab 2023-12-22 09:29:47 +08:00
qhy040404
7320cf7dd0 owner 2023-12-22 09:21:52 +08:00
qhy040404
bc6d03e442 Update MGMT-publish.yml 2023-12-22 09:20:30 +08:00
Masterain
307a49b346 Merge pull request #1191 from DGP-Studio/Masterain98-patch-2
Update .gitlab-ci.yml
2023-12-19 18:08:36 -08:00
Masterain
9f8d80ff43 Update .gitlab-ci.yml 2023-12-19 13:48:49 -08:00
Masterain
d34130b6c0 Update .gitlab-ci.yml 2023-12-17 02:44:15 -08:00
Masterain
2161f12069 Merge pull request #1187 from DGP-Studio/Masterain98-patch-1
Create .gitlab-ci.yml
2023-12-16 22:34:56 -08:00
Masterain
0bedd1894c Create .gitlab-ci.yml 2023-12-16 22:33:43 -08:00
DismissedLight
442db0bae4 Merge pull request #1184 from DGP-Studio/develop 2023-12-16 15:07:24 +08:00
92 changed files with 1122 additions and 1317 deletions

View File

@@ -22,7 +22,7 @@ body:
- label: 我知道文档站的导航栏中有**搜索功能**,且已经搜索过相关关键词 - label: 我知道文档站的导航栏中有**搜索功能**,且已经搜索过相关关键词
required: true required: true
- label: 我的问题不是[已修复](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E4%BF%AE%E5%A4%8D)的问题也不是一个别人已发布的**重复的**问题 - label: 我的问题不是[已完成](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E5%AE%8C%E6%88%90)的问题也不是一个别人已发布的**重复的**问题
required: true required: true
- type: input - type: input
@@ -51,7 +51,7 @@ body:
description: | description: |
在胡桃工具箱的设置界面,你可以找到并复制你的设备 ID 在胡桃工具箱的设置界面,你可以找到并复制你的设备 ID
如果你的问题涉及程序崩溃,请填写该项,这将有助于我们定位问题 如果你的问题涉及程序崩溃,请填写该项,这将有助于我们定位问题
如果你的程序已经无法启动,请下载并运行[此PowerShell脚本](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1),它将显示你的设备 ID 如果你的程序已经无法启动,请下载并运行[此工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe),它将显示你的设备 ID
validations: validations:
required: false required: false
@@ -87,7 +87,7 @@ body:
label: 发生了什么? label: 发生了什么?
description: | description: |
详细的描述问题发生前后的行为,以便我们解决问题。**如果你的问题涉及程序崩溃,你应当检查 Windows 事件查看器,并将相关的 `.Net 错误`详情附上** 详细的描述问题发生前后的行为,以便我们解决问题。**如果你的问题涉及程序崩溃,你应当检查 Windows 事件查看器,并将相关的 `.Net 错误`详情附上**
如果你无法找到该日志,请下载并运行[此PowerShell脚本](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/dump_log_script/dump_log_zh.ps1),它将输出错误日志 如果你无法找到该日志,请下载并运行[此工具](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe),它将转储问题日志至工具运行目录中的 `Snap.Hutao Error Log.txt`
validations: validations:
required: true required: true

View File

@@ -22,7 +22,7 @@ body:
- label: I and tried **search feature** in Snap Hutao document site, and no associated article - label: I and tried **search feature** in Snap Hutao document site, and no associated article
required: true required: true
- label: My issue is not a [fixed issue](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E4%BF%AE%E5%A4%8D), and it's not a duplicated issue - label: My issue is not a [finished issue](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E5%AE%8C%E6%88%90), and it's not a duplicated issue
required: true required: true
- type: input - type: input
@@ -51,7 +51,7 @@ body:
description: | description: |
In Snap Hutao's settings page, you can find and copy your device ID In Snap Hutao's settings page, you can find and copy your device ID
If your issue is about program crash, please fill this so we can dump the log and locate the source easier If your issue is about program crash, please fill this so we can dump the log and locate the source easier
If your program cannot startup, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1), it will shows your device ID. If your program cannot startup, please download and run [this tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe), it will shows your device ID.
validations: validations:
required: false required: false
@@ -87,7 +87,7 @@ body:
label: What Happened? label: What Happened?
description: | description: |
Describe your issue in detail to help us identify the issue. **If your issue is about program crash, you should check Windows Event Viewer, and attach associated `.Net Error` details here**If your program cannot startup, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1), it will shows your device ID. Describe your issue in detail to help us identify the issue. **If your issue is about program crash, you should check Windows Event Viewer, and attach associated `.Net Error` details here**If your program cannot startup, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/get_device_id/GetHutaoDeviceId.ps1), it will shows your device ID.
If you cannot find it, please download and run [this PowerShell script](https://github.com/DGP-Studio/ISSUE_TEMPLATES/releases/download/dump_log_script/dump_log_en.ps1), it will dump the error log. If you cannot find it, please download and run [this tool](https://github.com/DGP-Automation/ISSUE_TEMPLATES/releases/download/diagnosis_tools/Snap.Hutao.DiagTools.exe), it will dump the error log to `Snap.Hutao Error Log.txt` in the working directory of the tool.
validations: validations:
required: true required: true

View File

@@ -12,50 +12,32 @@ body:
value: | value: |
## 创建版本 ## 创建版本
- [ ] 同步一次 [Crowdin](https://crowdin.com/project/snap-hutao) 翻译 - [ ] 同步一次 [Crowdin](https://crowdin.com/project/snap-hutao) 翻译
- [ ] 发布 RC 版本Optional - [ ] 发布 RC 版本Optional
- [ ] 合并入主分支 - [ ] 合并入主分支
- [ ] 整理更新内容,等待翻译 - [ ] 整理更新内容,等待翻译
- [ ] 打包
- [ ] 提交微软商店
- [ ] 包含更新日志
- [ ] 在 [Snap.Hutao.Docs@next-patch](https://github.com/DGP-Studio/Snap.Hutao.Docs/tree/next-patch) 分支更新文档并直接开 PR - [ ] 在 [Snap.Hutao.Docs@next-patch](https://github.com/DGP-Studio/Snap.Hutao.Docs/tree/next-patch) 分支更新文档并直接开 PR
- [ ] 更新日志 - [ ] 更新日志
- [ ] 功能文档更新 - [ ] 功能文档更新
## 发布版本 [半自动] ## 发布版本 [半自动]
- [ ] 在 GitHub 个人设置中更新 [Publish-Automate Beta PAT](https://github.com/settings/tokens?type=beta),有效期需小于预计发版需要天数 - [ ] 在 GitHub 个人设置中更新 [Publish-Automate Beta PAT](https://github.com/settings/tokens?type=beta),有效期需小于预计发版需要天数
- [ ] 将更新的 PAT 更新至 Publish-Automate 库的 [Actions Secrets](https://github.com/DGP-Studio/Publish-Automate/settings/secrets/actions) 中 - [ ] 将更新的 PAT 更新至 Publish-Automate 库的 [Actions Secrets](https://github.com/DGP-Automation/Publish-Automate/settings/secrets/actions) 中
*** ***
- [ ] 运行 [Auto Publish Action](https://github.com/DGP-Studio/Publish-Automate/actions/workflows/auto-publish.yml) - [ ] 主分支合并入 release 分支
- [ ] 在 https://store.rg-adguard.net/ 下载新版本安装包 - [ ] 等待 Release 自动发布
- [ ] Store URL: https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52 - [ ] 检查极狐是否同步完成 Release
- [ ] 命名格式为 `Snap.Hutao x.x.x.msix`
- [ ] Merge 文档 PR - [ ] 通知用户
- [ ] 发布 Release
- [ ] 更新日志格式(以 1.6.2 版本为例)
```jsx
## Update log
https://hut.ao/en/statements/update-log.html#_1-6-2
## 更新日志
[此处从文档复制]
## What's Changed
**Full Changelog**: https://github.com/DGP-Studio/Snap.Hutao/compare/1.6.0...1.6.2
```
- [ ] 通知用户
- type: checkboxes - type: checkboxes
id: checklist-final id: checklist-final
attributes: attributes:
label: Final Check label: Final Check
description: Understand what you are doing description: Understand what you are doing
options: options:
- label: I understand that I will get banned from repository if I don't have permission to use this template - label: I understand that I will get banned from repository if I don't have permission to use this template
required: true required: true

62
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,62 @@
stages:
- fetch
- release
Fetch:
stage: fetch
rules:
- if: $CI_COMMIT_TAG
tags:
- us3
script:
- apt-get update -qy
- apt-get install -y curl jq
- RELEASE_INFO=$(curl -sSL "https://api.github.com/repos/$CI_PROJECT_PATH/releases/latest")
- ASSET_URL=$(echo "$RELEASE_INFO" | jq -r '.assets[] | select(.name | endswith(".msix")) | .browser_download_url')
- SHA256SUMS_URL=$(echo "$RELEASE_INFO" | jq -r '.assets[] | select(.name == "SHA256SUMS") | .browser_download_url')
- curl -LJO "$ASSET_URL"
- curl -LJO "$SHA256SUMS_URL"
- FILE_NAME=$(basename "$ASSET_URL")
- SHA256SUMS_NAME=$(basename "$SHA256SUMS_URL")
- echo "File name at script stage is $FILE_NAME"
- echo "SHA256SUMS name at script stage is $SHA256SUMS_NAME"
- echo "THIS_FILE_NAME=$FILE_NAME" >> next.env
- echo "THIS_SHA256SUMS_NAME=$SHA256SUMS_NAME" >> next.env
after_script:
- echo "Current Job ID is $CI_JOB_ID"
- echo "THIS_JOB_ID=$CI_JOB_ID" >> next.env
artifacts:
paths:
- "*.msix"
- "SHA256SUMS"
expire_in: 180 days
reports:
dotenv: next.env
release:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
rules:
- if: $CI_COMMIT_TAG
needs:
- job: Fetch
artifacts: true
variables:
TAG: '$CI_COMMIT_TAG'
script:
- echo "Create Release $TAG"
- echo "$THIS_JOB_ID"
- echo "$THIS_FILE_NAME"
release:
name: '$TAG'
tag_name: '$TAG'
ref: '$TAG'
description: 'Release $TAG by CI'
assets:
links:
- name: "$THIS_FILE_NAME"
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/raw/$THIS_FILE_NAME?inline=false"
link_type: package
- name: "$THIS_SHA256SUMS_NAME"
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/raw/$THIS_SHA256SUMS_NAME?inline=false"
link_type: other

View File

@@ -61,7 +61,6 @@ FileSaveDialog
IFileOpenDialog IFileOpenDialog
IFileSaveDialog IFileSaveDialog
IPersistFile IPersistFile
IShellLinkDataList
IShellLinkW IShellLinkW
ShellLink ShellLink
SHELL_LINK_DATA_FLAGS SHELL_LINK_DATA_FLAGS
@@ -70,7 +69,6 @@ SHELL_LINK_DATA_FLAGS
IMemoryBufferByteAccess IMemoryBufferByteAccess
// Const value // Const value
E_FAIL
INFINITE INFINITE
RPC_E_WRONG_THREAD RPC_E_WRONG_THREAD
MAX_PATH MAX_PATH

View File

@@ -18,10 +18,4 @@
TargetType="FlyoutPresenter"> TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="6"/> <Setter Property="Padding" Value="6"/>
</Style> </Style>
<Style
x:Key="FlyoutPresenterPadding16And10Style"
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="16,10"/>
</Style>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -31,5 +31,4 @@
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String> <x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String> <x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
<x:String x:Key="UI_EmotionIcon293">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String> <x:String x:Key="UI_EmotionIcon293">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
<x:String x:Key="UI_EmotionIcon445">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -18,11 +18,11 @@ internal sealed class CommandLineBuilder
/// <summary> /// <summary>
/// 当符合条件时添加参数 /// 当符合条件时添加参数
/// </summary> /// </summary>
/// <param name="condition">条件</param>
/// <param name="name">参数名称</param> /// <param name="name">参数名称</param>
/// <param name="condition">条件</param>
/// <param name="value">值</param> /// <param name="value">值</param>
/// <returns>命令行建造器</returns> /// <returns>命令行建造器</returns>
public CommandLineBuilder AppendIf(bool condition, string name, object? value = null) public CommandLineBuilder AppendIf(string name, bool condition, object? value = null)
{ {
return condition ? Append(name, value) : this; return condition ? Append(name, value) : this;
} }
@@ -35,7 +35,7 @@ internal sealed class CommandLineBuilder
/// <returns>命令行建造器</returns> /// <returns>命令行建造器</returns>
public CommandLineBuilder AppendIfNotNull(string name, object? value = null) public CommandLineBuilder AppendIfNotNull(string name, object? value = null)
{ {
return AppendIf(value is not null, name, value); return AppendIf(name, value is not null, value);
} }
/// <summary> /// <summary>

View File

@@ -1,23 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
/// <summary>
/// 指示此特性附加的属性会在属性改变后会异步地设置的其他属性
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
internal sealed class AlsoAsyncSetsAttribute : Attribute
{
public AlsoAsyncSetsAttribute(string propertyName)
{
}
public AlsoAsyncSetsAttribute(string propertyName1, string propertyName2)
{
}
public AlsoAsyncSetsAttribute(params string[] propertyNames)
{
}
}

View File

@@ -1,23 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
/// <summary>
/// 指示此特性附加的属性会在属性改变后会设置的其他属性
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
internal sealed class AlsoSetsAttribute : Attribute
{
public AlsoSetsAttribute(string propertyName)
{
}
public AlsoSetsAttribute(string propertyName1, string propertyName2)
{
}
public AlsoSetsAttribute(params string[] propertyNames)
{
}
}

View File

@@ -1,20 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
internal sealed class HutaoException : Exception
{
public HutaoException(HutaoExceptionKind kind, string message, Exception? innerException)
: this(message, innerException)
{
Kind = kind;
}
public HutaoException(string message, Exception? innerException)
: base($"{message}\n{innerException?.Message}", innerException)
{
}
public HutaoExceptionKind Kind { get; private set; }
}

View File

@@ -1,9 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
internal enum HutaoExceptionKind
{
None,
}

View File

@@ -49,13 +49,6 @@ internal static class ThrowHelper
throw new NotSupportedException(); throw new NotSupportedException();
} }
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static NotSupportedException NotSupported(string message)
{
throw new NotSupportedException(message);
}
[DoesNotReturn] [DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)] [MethodImpl(MethodImplOptions.NoInlining)]
public static OperationCanceledException OperationCanceled(string message, Exception? inner = default) public static OperationCanceledException OperationCanceled(string message, Exception? inner = default)

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core;
internal static class RuntimeOptionsExtension
{
public static string GetDataFolderUpdateCacheFolderFile(this RuntimeOptions options, string fileName)
{
string directory = Path.Combine(options.DataFolder, "UpdateCache");
Directory.CreateDirectory(directory);
return Path.Combine(directory, fileName);
}
}

View File

@@ -7,30 +7,30 @@ namespace Snap.Hutao.Core.Setting;
/// 设置键 /// 设置键
/// </summary> /// </summary>
[HighQuality] [HighQuality]
[SuppressMessage("", "SA1124")]
internal static class SettingKeys internal static class SettingKeys
{ {
#region MainWindow
public const string WindowRect = "WindowRect"; public const string WindowRect = "WindowRect";
public const string IsNavPaneOpen = "IsNavPaneOpen"; public const string IsNavPaneOpen = "IsNavPaneOpen";
public const string IsInfoBarToggleChecked = "IsInfoBarToggleChecked";
public const string ExcludedAnnouncementIds = "ExcludedAnnouncementIds";
#endregion
#region Application
public const string LaunchTimes = "LaunchTimes"; public const string LaunchTimes = "LaunchTimes";
public const string DataFolderPath = "DataFolderPath"; public const string DataFolderPath = "DataFolderPath";
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled";
#endregion
#region Passport
public const string PassportUserName = "PassportUserName"; public const string PassportUserName = "PassportUserName";
public const string PassportPassword = "PassportPassword";
#endregion
#region Cultivation public const string PassportPassword = "PassportPassword";
public const string IsInfoBarToggleChecked = "IsInfoBarToggleChecked";
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
public const string ExcludedAnnouncementIds = "ExcludedAnnouncementIds";
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
public const string OverrideElevationRequirement = "OverrideElevationRequirement";
public const string CultivationAvatarLevelCurrent = "CultivationAvatarLevelCurrent"; public const string CultivationAvatarLevelCurrent = "CultivationAvatarLevelCurrent";
public const string CultivationAvatarLevelTarget = "CultivationAvatarLevelTarget"; public const string CultivationAvatarLevelTarget = "CultivationAvatarLevelTarget";
public const string CultivationAvatarSkillACurrent = "CultivationAvatarSkillACurrent"; public const string CultivationAvatarSkillACurrent = "CultivationAvatarSkillACurrent";
@@ -43,18 +43,13 @@ internal static class SettingKeys
public const string CultivationWeapon90LevelTarget = "CultivationWeapon90LevelTarget"; public const string CultivationWeapon90LevelTarget = "CultivationWeapon90LevelTarget";
public const string CultivationWeapon70LevelCurrent = "CultivationWeapon70LevelCurrent"; public const string CultivationWeapon70LevelCurrent = "CultivationWeapon70LevelCurrent";
public const string CultivationWeapon70LevelTarget = "CultivationWeapon70LevelTarget"; public const string CultivationWeapon70LevelTarget = "CultivationWeapon70LevelTarget";
#endregion
#region HomeCard Dashboard
public const string IsHomeCardLaunchGamePresented = "IsHomeCardLaunchGamePresented"; public const string IsHomeCardLaunchGamePresented = "IsHomeCardLaunchGamePresented";
public const string IsHomeCardGachaStatisticsPresented = "IsHomeCardGachaStatisticsPresented"; public const string IsHomeCardGachaStatisticsPresented = "IsHomeCardGachaStatisticsPresented";
public const string IsHomeCardAchievementPresented = "IsHomeCardAchievementPresented"; public const string IsHomeCardAchievementPresented = "IsHomeCardAchievementPresented";
public const string IsHomeCardDailyNotePresented = "IsHomeCardDailyNotePresented"; public const string IsHomeCardDailyNotePresented = "IsHomeCardDailyNotePresented";
#endregion
#region DevTool public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
public const string OverrideElevationRequirement = "OverrideElevationRequirement"; public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled";
public const string OverrideUpdateVersionComparison = "OverrideUpdateVersionComparison";
#endregion
} }

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Service;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Windows.Storage; using Windows.Storage;
@@ -18,16 +19,23 @@ namespace Snap.Hutao.Core.Shell;
internal sealed partial class ShellLinkInterop : IShellLinkInterop internal sealed partial class ShellLinkInterop : IShellLinkInterop
{ {
private readonly RuntimeOptions runtimeOptions; private readonly RuntimeOptions runtimeOptions;
private readonly AppOptions appOptions;
public async ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync() public async ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync()
{ {
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
string targetLogoPath = Path.Combine(runtimeOptions.DataFolder, "ShellLinkLogo.ico"); string targetLogoPath = Path.Combine(runtimeOptions.DataFolder, "ShellLinkLogo.ico");
try try
{ {
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
StorageFile iconFile = await StorageFile.GetFileFromApplicationUriAsync(sourceLogoUri); StorageFile iconFile = await StorageFile.GetFileFromApplicationUriAsync(sourceLogoUri);
await iconFile.OverwriteCopyAsync(targetLogoPath).ConfigureAwait(false); using (Stream inputStream = (await iconFile.OpenReadAsync()).AsStream())
{
using (FileStream outputStream = File.Create(targetLogoPath))
{
await inputStream.CopyToAsync(outputStream).ConfigureAwait(false);
}
}
} }
catch catch
{ {
@@ -37,15 +45,12 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
HRESULT result = CoCreateInstance<ShellLink, IShellLinkW>(null, CLSCTX.CLSCTX_INPROC_SERVER, out IShellLinkW shellLink); HRESULT result = CoCreateInstance<ShellLink, IShellLinkW>(null, CLSCTX.CLSCTX_INPROC_SERVER, out IShellLinkW shellLink);
Marshal.ThrowExceptionForHR(result); Marshal.ThrowExceptionForHR(result);
shellLink.SetPath($"shell:AppsFolder\\{runtimeOptions.FamilyName}!App"); shellLink.SetPath(appOptions.PowerShellPath);
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL); shellLink.SetArguments($"""
-Command "Start-Process shell:AppsFolder\{runtimeOptions.FamilyName}!App -verb runas"
""");
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_SHOWMINNOACTIVE);
shellLink.SetIconLocation(targetLogoPath, 0); shellLink.SetIconLocation(targetLogoPath, 0);
IShellLinkDataList shellLinkDataList = (IShellLinkDataList)shellLink;
shellLinkDataList.GetFlags(out uint flags);
flags |= (uint)SHELL_LINK_DATA_FLAGS.SLDF_RUNAS_USER;
shellLinkDataList.SetFlags(flags);
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk"); string target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk");

View File

@@ -1,24 +1,45 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.UI.Dispatching;
namespace Snap.Hutao.Core.Threading; namespace Snap.Hutao.Core.Threading;
internal class DispatcherQueueProgress<T> : IProgress<T> internal class DispatcherQueueProgress<T> : IProgress<T>
{ {
private readonly DispatcherQueue dispatcherQueue; private readonly SynchronizationContext synchronizationContext;
private readonly Action<T> handler; private readonly Action<T>? handler;
private readonly SendOrPostCallback invokeHandlers;
public DispatcherQueueProgress(Action<T> handler, DispatcherQueue dispatcherQueue) public DispatcherQueueProgress(Action<T> handler, SynchronizationContext synchronizationContext)
{ {
this.dispatcherQueue = dispatcherQueue; this.synchronizationContext = synchronizationContext;
invokeHandlers = new SendOrPostCallback(InvokeHandlers);
ArgumentNullException.ThrowIfNull(handler);
this.handler = handler; this.handler = handler;
} }
public event EventHandler<T>? ProgressChanged;
public void Report(T value) public void Report(T value)
{ {
Action<T> handler = this.handler; Action<T>? handler = this.handler;
dispatcherQueue.TryEnqueue(() => handler(value)); EventHandler<T>? changedEvent = ProgressChanged;
if (handler is not null || changedEvent is not null)
{
synchronizationContext.Post(invokeHandlers, value);
}
}
[SuppressMessage("", "SH007")]
private void InvokeHandlers(object? state)
{
T value = (T)state!;
Action<T>? handler = this.handler;
EventHandler<T>? changedEvent = ProgressChanged;
handler?.Invoke(value);
changedEvent?.Invoke(this, value);
} }
} }

View File

@@ -8,6 +8,8 @@ namespace Snap.Hutao.Core.Threading;
/// </summary> /// </summary>
internal interface ITaskContext internal interface ITaskContext
{ {
SynchronizationContext SynchronizationContext { get; }
void BeginInvokeOnMainThread(Action action); void BeginInvokeOnMainThread(Action action);
void InvokeOnMainThread(Action action); void InvokeOnMainThread(Action action);

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Dispatching;
namespace Snap.Hutao.Core.Threading;
internal interface ITaskContextUnsafe
{
DispatcherQueue DispatcherQueue { get; }
}

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.Threading;
/// 任务上下文 /// 任务上下文
/// </summary> /// </summary>
[Injection(InjectAs.Singleton, typeof(ITaskContext))] [Injection(InjectAs.Singleton, typeof(ITaskContext))]
internal sealed class TaskContext : ITaskContext, ITaskContextUnsafe internal sealed class TaskContext : ITaskContext
{ {
private readonly DispatcherQueueSynchronizationContext synchronizationContext; private readonly DispatcherQueueSynchronizationContext synchronizationContext;
private readonly DispatcherQueue dispatcherQueue; private readonly DispatcherQueue dispatcherQueue;
@@ -24,7 +24,7 @@ internal sealed class TaskContext : ITaskContext, ITaskContextUnsafe
SynchronizationContext.SetSynchronizationContext(synchronizationContext); SynchronizationContext.SetSynchronizationContext(synchronizationContext);
} }
public DispatcherQueue DispatcherQueue { get => dispatcherQueue; } public SynchronizationContext SynchronizationContext { get => synchronizationContext; }
/// <inheritdoc/> /// <inheritdoc/>
public ThreadPoolSwitchOperation SwitchToBackgroundAsync() public ThreadPoolSwitchOperation SwitchToBackgroundAsync()

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Threading;
internal sealed class Throttler
{
private readonly ConcurrentDictionary<string, SemaphoreSlim> methodSemaphoreMap = new();
public ValueTask<SemaphoreSlimToken> ThrottleAsync(CancellationToken token = default, [CallerMemberName] string callerName = default!, [CallerLineNumber] int callerLine = 0)
{
string key = $"{callerName}L{callerLine}";
SemaphoreSlim semaphore = methodSemaphoreMap.GetOrAdd(key, name => new SemaphoreSlim(1));
return semaphore.EnterAsync(token);
}
}

View File

@@ -1,21 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
using Windows.Storage;
namespace Snap.Hutao.Extension;
internal static class StorageFileExtension
{
public static async ValueTask OverwriteCopyAsync(this StorageFile file, string targetFile)
{
using (Stream outputStream = (await file.OpenReadAsync()).AsStreamForRead())
{
using (FileStream inputStream = File.Create(targetFile))
{
await outputStream.CopyToAsync(inputStream).ConfigureAwait(false);
}
}
}
}

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.Factory.Progress; namespace Snap.Hutao.Factory.Progress;
[ConstructorGenerated] [ConstructorGenerated]
@@ -13,11 +11,6 @@ internal sealed partial class ProgressFactory : IProgressFactory
public IProgress<T> CreateForMainThread<T>(Action<T> handler) public IProgress<T> CreateForMainThread<T>(Action<T> handler)
{ {
if (taskContext is not ITaskContextUnsafe @unsafe) return new DispatcherQueueProgress<T>(handler, taskContext.SynchronizationContext);
{
throw ThrowHelper.NotSupported();
}
return new DispatcherQueueProgress<T>(handler, @unsafe.DispatcherQueue);
} }
} }

View File

@@ -34,7 +34,7 @@
<ListView <ListView
Grid.Row="1" Grid.Row="1"
ItemsSource="{Binding GameAccountsView}" ItemsSource="{Binding GameAccounts}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"> SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}">
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate> <DataTemplate>

View File

@@ -2,7 +2,9 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Snap.Hutao.Control;
using Snap.Hutao.Core.Windowing; using Snap.Hutao.Core.Windowing;
using Windows.Foundation;
using Windows.Win32.UI.WindowsAndMessaging; using Windows.Win32.UI.WindowsAndMessaging;
namespace Snap.Hutao; namespace Snap.Hutao;

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.Model.Entity;
/// </summary> /// </summary>
[HighQuality] [HighQuality]
[Table("game_accounts")] [Table("game_accounts")]
internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount, string, string, SchemeType> internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount, string, string>
{ {
/// <summary> /// <summary>
/// 内部Id /// 内部Id
@@ -40,17 +40,21 @@ internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount,
/// <summary> /// <summary>
/// [MIHOYOSDK_ADL_PROD_CN_h3123967166] /// [MIHOYOSDK_ADL_PROD_CN_h3123967166]
/// [MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810]
/// </summary> /// </summary>
public string MihoyoSDK { get; set; } = default!; public string MihoyoSDK { get; set; } = default!;
public static GameAccount From(string name, string sdk, SchemeType type) /// <summary>
/// 构造一个新的游戏内账号
/// </summary>
/// <param name="name">名称</param>
/// <param name="sdk">sdk</param>
/// <returns>游戏内账号</returns>
public static GameAccount From(string name, string sdk)
{ {
return new() return new()
{ {
Name = name, Name = name,
MihoyoSDK = sdk, MihoyoSDK = sdk,
Type = type,
}; };
} }

View File

@@ -9,18 +9,18 @@ namespace Snap.Hutao.Model.Entity.Primitive;
[HighQuality] [HighQuality]
internal enum SchemeType internal enum SchemeType
{ {
/// <summary>
/// 国服官服
/// </summary>
ChineseOfficial,
/// <summary> /// <summary>
/// 国际服 /// 国际服
/// </summary> /// </summary>
Oversea, Hoyoverse,
/// <summary>
/// 国服官服
/// </summary>
Official,
/// <summary> /// <summary>
/// 渠道服 /// 渠道服
/// </summary> /// </summary>
ChineseBilibili, Bilibili,
} }

View File

@@ -15,7 +15,9 @@ internal sealed partial class SettingEntry
public const string GamePathEntries = "GamePathEntries"; public const string GamePathEntries = "GamePathEntries";
[Obsolete("不再使用 PowerShell")] /// <summary>
/// PowerShell 路径
/// </summary>
public const string PowerShellPath = "PowerShellPath"; public const string PowerShellPath = "PowerShellPath";
/// <summary> /// <summary>

View File

@@ -93,10 +93,6 @@ internal static class AvatarIds
public static readonly AvatarId Neuvillette = 10000087; public static readonly AvatarId Neuvillette = 10000087;
public static readonly AvatarId Charlotte = 10000088; public static readonly AvatarId Charlotte = 10000088;
public static readonly AvatarId Furina = 10000089; public static readonly AvatarId Furina = 10000089;
public static readonly AvatarId Chevreuse = 10000090;
public static readonly AvatarId Navia = 10000091;
public static readonly AvatarId Gaming = 10000092;
public static readonly AvatarId Xianyun = 10000093;
/// <summary> /// <summary>
/// 检查该角色是否为主角 /// 检查该角色是否为主角

View File

@@ -4,22 +4,20 @@
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10" xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
IgnorableNamespaces="com uap desktop desktop6 rescap mp"> IgnorableNamespaces="com uap desktop rescap mp">
<Identity <Identity
Name="60568DGPStudio.SnapHutao" Name="60568DGPStudio.SnapHutao"
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52" Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
Version="1.9.1.0" /> Version="1.9.0.0" />
<Properties> <Properties>
<DisplayName>Snap Hutao</DisplayName> <DisplayName>Snap Hutao</DisplayName>
<PublisherDisplayName>DGP Studio</PublisherDisplayName> <PublisherDisplayName>DGP Studio</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo> <Logo>Assets\StoreLogo.png</Logo>
<desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
</Properties> </Properties>
<Dependencies> <Dependencies>
@@ -66,6 +64,5 @@
<Capabilities> <Capabilities>
<rescap:Capability Name="runFullTrust"/> <rescap:Capability Name="runFullTrust"/>
<rescap:Capability Name="unvirtualizedResources"/>
</Capabilities> </Capabilities>
</Package> </Package>

View File

@@ -4,22 +4,20 @@
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10" xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
IgnorableNamespaces="com uap desktop desktop6 rescap mp"> IgnorableNamespaces="com uap desktop rescap mp">
<Identity <Identity
Name="60568DGPStudio.SnapHutaoDev" Name="60568DGPStudio.SnapHutaoDev"
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52" Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
Version="1.9.1.0" /> Version="1.9.0.0" />
<Properties> <Properties>
<DisplayName>Snap Hutao Dev</DisplayName> <DisplayName>Snap Hutao Dev</DisplayName>
<PublisherDisplayName>DGP Studio</PublisherDisplayName> <PublisherDisplayName>DGP Studio</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo> <Logo>Assets\StoreLogo.png</Logo>
<desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
</Properties> </Properties>
<Dependencies> <Dependencies>
@@ -66,6 +64,5 @@
<Capabilities> <Capabilities>
<rescap:Capability Name="runFullTrust"/> <rescap:Capability Name="runFullTrust"/>
<rescap:Capability Name="unvirtualizedResources"/>
</Capabilities> </Capabilities>
</Package> </Package>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
<xsd:choice maxOccurs="unbounded"> <xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata"> <xsd:element name="metadata">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" /> <xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="assembly"> <xsd:element name="assembly">
<xsd:complexType> <xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" /> <xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="data"> <xsd:element name="data">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="resheader"> <xsd:element name="resheader">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -186,6 +186,9 @@
<data name="FilePickerImportCommit" xml:space="preserve"> <data name="FilePickerImportCommit" xml:space="preserve">
<value>Import</value> <value>Import</value>
</data> </data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>Select PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve"> <data name="GuideWindowTitle" xml:space="preserve">
<value>Welcome to Snap Hutao, Traveler ~</value> <value>Welcome to Snap Hutao, Traveler ~</value>
</data> </data>
@@ -929,6 +932,9 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve"> <data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>Unable to set registry key without enabling long path</value> <value>Unable to set registry key without enabling long path</value>
</data> </data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>PowerShell installation directory not found</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve"> <data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>Unable to read game config file {0}, file may be not exist</value> <value>Unable to read game config file {0}, file may be not exist</value>
</data> </data>
@@ -2426,6 +2432,12 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve"> <data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>When setting the game path, please select the game program (Yuanshen.exe or GenshinImpact.exe) instead of the game launcher (launcher.exe)</value> <value>When setting the game path, please select the game program (Yuanshen.exe or GenshinImpact.exe) instead of the game launcher (launcher.exe)</value>
</data> </data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>Snap Hutao uses PowerShell to modify information in registry to change game accounts</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell Path</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve"> <data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell Experience</value> <value>Shell Experience</value>
</data> </data>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
<xsd:choice maxOccurs="unbounded"> <xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata"> <xsd:element name="metadata">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" /> <xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="assembly"> <xsd:element name="assembly">
<xsd:complexType> <xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" /> <xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="data"> <xsd:element name="data">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="resheader"> <xsd:element name="resheader">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -186,6 +186,9 @@
<data name="FilePickerImportCommit" xml:space="preserve"> <data name="FilePickerImportCommit" xml:space="preserve">
<value>Impor</value> <value>Impor</value>
</data> </data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>Pilih PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve"> <data name="GuideWindowTitle" xml:space="preserve">
<value>Selamat Datang di Snap Hutao, Traveler ~</value> <value>Selamat Datang di Snap Hutao, Traveler ~</value>
</data> </data>
@@ -929,6 +932,9 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve"> <data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>Tidak dapat mengatur kunci registri tanpa mengaktifkan path panjang</value> <value>Tidak dapat mengatur kunci registri tanpa mengaktifkan path panjang</value>
</data> </data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>Direktori instalasi PowerShell tidak ditemukan</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve"> <data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>Tidak dapat membaca file konfigurasi game {0}, file mungkin tidak ada</value> <value>Tidak dapat membaca file konfigurasi game {0}, file mungkin tidak ada</value>
</data> </data>
@@ -2426,6 +2432,12 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve"> <data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>Saat mengatur jalur permainan, pilih program permainan (Yuanshen.exe atau GenshinImpact.exe) bukan peluncur permainan (launcher.exe)</value> <value>Saat mengatur jalur permainan, pilih program permainan (Yuanshen.exe atau GenshinImpact.exe) bukan peluncur permainan (launcher.exe)</value>
</data> </data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>Snap Hutao menggunakan PowerShell untuk memodifikasi informasi di registri untuk mengubah akun Game</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>Path PowerShell</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve"> <data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Pengalaman Shell</value> <value>Pengalaman Shell</value>
</data> </data>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
<xsd:choice maxOccurs="unbounded"> <xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata"> <xsd:element name="metadata">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" /> <xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="assembly"> <xsd:element name="assembly">
<xsd:complexType> <xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" /> <xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="data"> <xsd:element name="data">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="resheader"> <xsd:element name="resheader">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -186,6 +186,9 @@
<data name="FilePickerImportCommit" xml:space="preserve"> <data name="FilePickerImportCommit" xml:space="preserve">
<value>インポート</value> <value>インポート</value>
</data> </data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>PowerShellを選択</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve"> <data name="GuideWindowTitle" xml:space="preserve">
<value>胡桃へようこそ</value> <value>胡桃へようこそ</value>
</data> </data>
@@ -929,6 +932,9 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve"> <data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>長いパスのサポートがオフになっているため、レジストリキーを編集できません。</value> <value>長いパスのサポートがオフになっているため、レジストリキーを編集できません。</value>
</data> </data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>PowerShellのインストールディレクトリが見つかりません</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve"> <data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>ゲーム設定ファイル {0} の読み込みに失敗しました。ファイルが存在していない可能性があります。</value> <value>ゲーム設定ファイル {0} の読み込みに失敗しました。ファイルが存在していない可能性があります。</value>
</data> </data>
@@ -2426,6 +2432,12 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve"> <data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>ゲームのパスを設定する際、本体YuanShen.exe または GenshinImpact.exeを選んでください。ランチャーlauncher.exeではありません</value> <value>ゲームのパスを設定する際、本体YuanShen.exe または GenshinImpact.exeを選んでください。ランチャーlauncher.exeではありません</value>
</data> </data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>胡桃のゲームランチャーはPowershellを介してレジストリを変更し、ゲームで使用するアカウントを変更します。</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell パス</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve"> <data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell エクスペリエンス</value> <value>Shell エクスペリエンス</value>
</data> </data>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
<xsd:choice maxOccurs="unbounded"> <xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata"> <xsd:element name="metadata">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" /> <xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="assembly"> <xsd:element name="assembly">
<xsd:complexType> <xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" /> <xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="data"> <xsd:element name="data">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="resheader"> <xsd:element name="resheader">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -186,6 +186,9 @@
<data name="FilePickerImportCommit" xml:space="preserve"> <data name="FilePickerImportCommit" xml:space="preserve">
<value>가져오기</value> <value>가져오기</value>
</data> </data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>选择 PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve"> <data name="GuideWindowTitle" xml:space="preserve">
<value>欢迎使用胡桃</value> <value>欢迎使用胡桃</value>
</data> </data>
@@ -929,6 +932,9 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve"> <data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>긴 경로 기능이 켜지지 않아 레지스트리 키 값을 설정할 수 없습니다</value> <value>긴 경로 기능이 켜지지 않아 레지스트리 키 값을 설정할 수 없습니다</value>
</data> </data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>PowerShell 설치 경로를 찾을 수 없습니다</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve"> <data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>无法读取游戏配置文件 {0},可能是文件不存在</value> <value>无法读取游戏配置文件 {0},可能是文件不存在</value>
</data> </data>
@@ -2426,6 +2432,12 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve"> <data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>게임 경로를 설정할 때 런쳐(launcher.exe) 대신 게임(YuanShen.exe 또는 GenshinImpact.exe)를 선택하세요</value> <value>게임 경로를 설정할 때 런쳐(launcher.exe) 대신 게임(YuanShen.exe 또는 GenshinImpact.exe)를 선택하세요</value>
</data> </data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>胡桃使用 PowerShell 更改注册表中的信息以修改游戏内账号</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell 路径</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve"> <data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell 体验</value> <value>Shell 体验</value>
</data> </data>

View File

@@ -120,12 +120,6 @@
<data name="AppDevNameAndVersion" xml:space="preserve"> <data name="AppDevNameAndVersion" xml:space="preserve">
<value>胡桃 Dev {0}</value> <value>胡桃 Dev {0}</value>
</data> </data>
<data name="AppElevatedDevNameAndVersion" xml:space="preserve">
<value>胡桃Dev {0} [管理员]</value>
</data>
<data name="AppElevatedNameAndVersion" xml:space="preserve">
<value>胡桃 {0} [管理员]</value>
</data>
<data name="AppName" xml:space="preserve"> <data name="AppName" xml:space="preserve">
<value>胡桃</value> <value>胡桃</value>
</data> </data>
@@ -192,6 +186,9 @@
<data name="FilePickerImportCommit" xml:space="preserve"> <data name="FilePickerImportCommit" xml:space="preserve">
<value>导入</value> <value>导入</value>
</data> </data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>选择 PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve"> <data name="GuideWindowTitle" xml:space="preserve">
<value>欢迎使用胡桃</value> <value>欢迎使用胡桃</value>
</data> </data>
@@ -935,6 +932,9 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve"> <data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>未开启长路径功能,无法设置注册表键值</value> <value>未开启长路径功能,无法设置注册表键值</value>
</data> </data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>找不到 PowerShell 的安装目录</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve"> <data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>无法读取游戏配置文件 {0},可能是文件不存在</value> <value>无法读取游戏配置文件 {0},可能是文件不存在</value>
</data> </data>
@@ -1553,9 +1553,6 @@
<data name="ViewModelLaunchGameSwitchGameAccountFail" xml:space="preserve"> <data name="ViewModelLaunchGameSwitchGameAccountFail" xml:space="preserve">
<value>切换账号失败</value> <value>切换账号失败</value>
</data> </data>
<data name="ViewModelLaunchGameUnableToSwitchUidAttachedGameAccount" xml:space="preserve">
<value>无法选择UID [{0}] 对应的账号 [{1}],该账号不属于当前服务器</value>
</data>
<data name="ViewModelSettingActionComplete" xml:space="preserve"> <data name="ViewModelSettingActionComplete" xml:space="preserve">
<value>操作完成</value> <value>操作完成</value>
</data> </data>
@@ -2147,9 +2144,6 @@
<data name="ViewPageLaunchGameResourcePreDownloadHeader" xml:space="preserve"> <data name="ViewPageLaunchGameResourcePreDownloadHeader" xml:space="preserve">
<value>预下载</value> <value>预下载</value>
</data> </data>
<data name="ViewPageLaunchGameSelectGamePath" xml:space="preserve">
<value>选择游戏路径</value>
</data>
<data name="ViewPageLaunchGameSwitchAccountAttachUidNull" xml:space="preserve"> <data name="ViewPageLaunchGameSwitchAccountAttachUidNull" xml:space="preserve">
<value>该账号尚未绑定实时便笺通知 UID</value> <value>该账号尚未绑定实时便笺通知 UID</value>
</data> </data>
@@ -2238,7 +2232,7 @@
<value>创建</value> <value>创建</value>
</data> </data>
<data name="ViewPageSettingCreateDesktopShortcutDescription" xml:space="preserve"> <data name="ViewPageSettingCreateDesktopShortcutDescription" xml:space="preserve">
<value>在桌面上创建默认以管理员身份启动的快捷方式</value> <value>在桌面上创建默认以管理员方式启动的快捷方式</value>
</data> </data>
<data name="ViewPageSettingCreateDesktopShortcutHeader" xml:space="preserve"> <data name="ViewPageSettingCreateDesktopShortcutHeader" xml:space="preserve">
<value>创建快捷方式</value> <value>创建快捷方式</value>
@@ -2282,15 +2276,6 @@
<data name="ViewPageSettingDeviceIpHeader" xml:space="preserve"> <data name="ViewPageSettingDeviceIpHeader" xml:space="preserve">
<value>设备 IP</value> <value>设备 IP</value>
</data> </data>
<data name="ViewPageSettingElevatedModeDescription" xml:space="preserve">
<value>管理员模式会影响部分功能的可用性与行为</value>
</data>
<data name="ViewPageSettingElevatedModeHeader" xml:space="preserve">
<value>管理员模式</value>
</data>
<data name="ViewPageSettingElevatedModeRestartAction" xml:space="preserve">
<value>以管理员身份重启</value>
</data>
<data name="ViewPageSettingEmptyHistoryVisibleDescription" xml:space="preserve"> <data name="ViewPageSettingEmptyHistoryVisibleDescription" xml:space="preserve">
<value>在祈愿记录页面显示或隐藏无记录的历史祈愿活动</value> <value>在祈愿记录页面显示或隐藏无记录的历史祈愿活动</value>
</data> </data>
@@ -2447,6 +2432,12 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve"> <data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>设置游戏路径时请选择游戏本体YuanShen.exe 或 GenshinImpact.exe而不是启动器launcher.exe</value> <value>设置游戏路径时请选择游戏本体YuanShen.exe 或 GenshinImpact.exe而不是启动器launcher.exe</value>
</data> </data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>胡桃使用 PowerShell 更改注册表中的信息以修改游戏内账号</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell 路径</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve"> <data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell 体验</value> <value>Shell 体验</value>
</data> </data>
@@ -2750,15 +2741,6 @@
<data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve"> <data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve">
<value>已复制到剪贴板</value> <value>已复制到剪贴板</value>
</data> </data>
<data name="WebDailyNoteArchonQuestStatusFinished" xml:space="preserve">
<value>全部完成</value>
</data>
<data name="WebDailyNoteArchonQuestStatusNotOpen" xml:space="preserve">
<value>尚未开启</value>
</data>
<data name="WebDailyNoteArchonQuestStatusOngoing" xml:space="preserve">
<value>进行中</value>
</data>
<data name="WebDailyNoteAttendanceRewardStatusFinishedNonReward" xml:space="preserve"> <data name="WebDailyNoteAttendanceRewardStatusFinishedNonReward" xml:space="preserve">
<value>已完成</value> <value>已完成</value>
</data> </data>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
<xsd:choice maxOccurs="unbounded"> <xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata"> <xsd:element name="metadata">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" /> <xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="assembly"> <xsd:element name="assembly">
<xsd:complexType> <xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" /> <xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="data"> <xsd:element name="data">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="resheader"> <xsd:element name="resheader">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -186,6 +186,9 @@
<data name="FilePickerImportCommit" xml:space="preserve"> <data name="FilePickerImportCommit" xml:space="preserve">
<value>Импорт</value> <value>Импорт</value>
</data> </data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>Выберите PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve"> <data name="GuideWindowTitle" xml:space="preserve">
<value>Добро пожаловать в Snap Hutao, путешественник ~</value> <value>Добро пожаловать в Snap Hutao, путешественник ~</value>
</data> </data>
@@ -929,6 +932,9 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve"> <data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>未开启长路径功能,无法设置注册表键值</value> <value>未开启长路径功能,无法设置注册表键值</value>
</data> </data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>找不到 PowerShell 的安装目录</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve"> <data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>无法读取游戏配置文件 {0},可能是文件不存在</value> <value>无法读取游戏配置文件 {0},可能是文件不存在</value>
</data> </data>
@@ -2426,6 +2432,12 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve"> <data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>设置游戏路径时请选择游戏本体YuanShen.exe 或 GenshinImpact.exe而不是启动器launcher.exe</value> <value>设置游戏路径时请选择游戏本体YuanShen.exe 或 GenshinImpact.exe而不是启动器launcher.exe</value>
</data> </data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>胡桃使用 PowerShell 更改注册表中的信息以修改游戏内账号</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell 路径</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve"> <data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell 体验</value> <value>Shell 体验</value>
</data> </data>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root"> <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType> <xsd:complexType>
<xsd:choice maxOccurs="unbounded"> <xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata"> <xsd:element name="metadata">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" /> <xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" /> <xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string" /> <xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="assembly"> <xsd:element name="assembly">
<xsd:complexType> <xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" /> <xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string" /> <xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="data"> <xsd:element name="data">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space" /> <xsd:attribute ref="xml:space"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
<xsd:element name="resheader"> <xsd:element name="resheader">
<xsd:complexType> <xsd:complexType>
<xsd:sequence> <xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType> </xsd:complexType>
</xsd:element> </xsd:element>
</xsd:choice> </xsd:choice>
@@ -186,6 +186,9 @@
<data name="FilePickerImportCommit" xml:space="preserve"> <data name="FilePickerImportCommit" xml:space="preserve">
<value>匯入</value> <value>匯入</value>
</data> </data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>選擇 PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve"> <data name="GuideWindowTitle" xml:space="preserve">
<value>歡迎使用胡桃</value> <value>歡迎使用胡桃</value>
</data> </data>
@@ -929,6 +932,9 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve"> <data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>未開啓長路徑功能,無法設定注冊表鍵值</value> <value>未開啓長路徑功能,無法設定注冊表鍵值</value>
</data> </data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>找不到 PowerShell 的安裝目錄</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve"> <data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>無法讀取遊戲設定檔 {0},可能是檔案不存在</value> <value>無法讀取遊戲設定檔 {0},可能是檔案不存在</value>
</data> </data>
@@ -2426,6 +2432,12 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve"> <data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>設置游戲路徑時請選擇游戲本體YuanShen.exe 或 GenshinImpact.exe 而不是啓動器launcher.exe</value> <value>設置游戲路徑時請選擇游戲本體YuanShen.exe 或 GenshinImpact.exe 而不是啓動器launcher.exe</value>
</data> </data>
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
<value>胡桃使用 PowerShell 更改註冊表中的信息以修改遊戲內賬號</value>
</data>
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
<value>PowerShell 路徑</value>
</data>
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve"> <data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
<value>Shell 體驗</value> <value>Shell 體驗</value>
</data> </data>

View File

@@ -7,6 +7,7 @@ using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab; using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement; using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
using Snap.Hutao.Web.Response; using Snap.Hutao.Web.Response;
using System.Globalization;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;

View File

@@ -1,12 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.Extensions.Primitives;
using Snap.Hutao.Core.Windowing; using Snap.Hutao.Core.Windowing;
using Snap.Hutao.Model; using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab; using Snap.Hutao.Web.Hoyolab;
using System.Globalization; using System.Globalization;
using System.IO;
namespace Snap.Hutao.Service; namespace Snap.Hutao.Service;
@@ -14,12 +16,42 @@ namespace Snap.Hutao.Service;
[Injection(InjectAs.Singleton)] [Injection(InjectAs.Singleton)]
internal sealed partial class AppOptions : DbStoreOptions internal sealed partial class AppOptions : DbStoreOptions
{ {
private string? powerShellPath;
private bool? isEmptyHistoryWishVisible; private bool? isEmptyHistoryWishVisible;
private BackdropType? backdropType; private BackdropType? backdropType;
private CultureInfo? currentCulture; private CultureInfo? currentCulture;
private Region? region; private Region? region;
private string? geetestCustomCompositeUrl; private string? geetestCustomCompositeUrl;
public string PowerShellPath
{
get
{
return GetOption(ref powerShellPath, SettingEntry.PowerShellPath, GetDefaultPowerShellLocationOrEmpty);
static string GetDefaultPowerShellLocationOrEmpty()
{
string? paths = Environment.GetEnvironmentVariable("Path");
if (!string.IsNullOrEmpty(paths))
{
foreach (StringSegment path in new StringTokenizer(paths, [';']))
{
if (path is { HasValue: true, Length: > 0 })
{
if (path.Value.Contains("WindowsPowerShell", StringComparison.OrdinalIgnoreCase))
{
return Path.Combine(path.Value, "powershell.exe");
}
}
}
}
return string.Empty;
}
}
set => SetOption(ref powerShellPath, SettingEntry.PowerShellPath, value);
}
public bool IsEmptyHistoryWishVisible public bool IsEmptyHistoryWishVisible
{ {
get => GetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible); get => GetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible);

View File

@@ -77,7 +77,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
List<DailyNoteEntry> entryList = await dailyNoteDbService.GetDailyNoteEntryIncludeUserListAsync().ConfigureAwait(false); List<DailyNoteEntry> entryList = await dailyNoteDbService.GetDailyNoteEntryIncludeUserListAsync().ConfigureAwait(false);
entryList.ForEach(entry => { entry.UserGameRole = userService.GetUserGameRoleByUid(entry.Uid); }); entryList.ForEach(entry => { entry.UserGameRole = userService.GetUserGameRoleByUid(entry.Uid); });
entries = entryList.ToObservableCollection(); entries = new(entryList);
} }
return entries; return entries;
@@ -147,7 +147,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
// 发送通知必须早于数据库更新,否则会导致通知重复 // 发送通知必须早于数据库更新,否则会导致通知重复
await dailyNoteNotificationOperation.SendAsync(entry).ConfigureAwait(false); await dailyNoteNotificationOperation.SendAsync(entry).ConfigureAwait(false);
await dailyNoteDbService.UpdateDailyNoteEntryAsync(entry).ConfigureAwait(false); await dailyNoteDbService.UpdateDailyNoteEntryAsync(entry).ConfigureAwait(false);
await dailyNoteWebhookOperation.TryPostDailyNoteToWebhookAsync(entry.Uid, dailyNote).ConfigureAwait(false); await dailyNoteWebhookOperation.TryPostDailyNoteToWebhookAsync(dailyNote).ConfigureAwait(false);
} }
} }
} }

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction; using Snap.Hutao.Web.Request.Builder.Abstraction;
using System.Net.Http; using System.Net.Http;
@@ -19,7 +18,7 @@ internal sealed partial class DailyNoteWebhookOperation
private readonly DailyNoteOptions dailyNoteOptions; private readonly DailyNoteOptions dailyNoteOptions;
private readonly HttpClient httpClient; private readonly HttpClient httpClient;
public async ValueTask TryPostDailyNoteToWebhookAsync(PlayerUid playerUid, WebDailyNote dailyNote, CancellationToken token = default) public async ValueTask TryPostDailyNoteToWebhookAsync(WebDailyNote dailyNote, CancellationToken token = default)
{ {
string? targetUrl = dailyNoteOptions.WebhookUrl; string? targetUrl = dailyNoteOptions.WebhookUrl;
if (string.IsNullOrEmpty(targetUrl) || !Uri.TryCreate(targetUrl, UriKind.Absolute, out Uri? targetUri)) if (string.IsNullOrEmpty(targetUrl) || !Uri.TryCreate(targetUrl, UriKind.Absolute, out Uri? targetUri))
@@ -29,7 +28,6 @@ internal sealed partial class DailyNoteWebhookOperation
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(targetUri) .SetRequestUri(targetUri)
.SetHeader("x-uid", $"{playerUid}")
.PostJson(dailyNote); .PostJson(dailyNote);
await builder.TryCatchSendAsync(httpClient, logger, token).ConfigureAwait(false); await builder.TryCatchSendAsync(httpClient, logger, token).ConfigureAwait(false);

View File

@@ -4,7 +4,6 @@
using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.View.Dialog; using Snap.Hutao.View.Dialog;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
@@ -17,6 +16,7 @@ internal sealed partial class GameAccountService : IGameAccountService
private readonly IContentDialogFactory contentDialogFactory; private readonly IContentDialogFactory contentDialogFactory;
private readonly IGameDbService gameDbService; private readonly IGameDbService gameDbService;
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
private readonly AppOptions appOptions;
private ObservableCollection<GameAccount>? gameAccounts; private ObservableCollection<GameAccount>? gameAccounts;
@@ -25,56 +25,77 @@ internal sealed partial class GameAccountService : IGameAccountService
get => gameAccounts ??= gameDbService.GetGameAccountCollection(); get => gameAccounts ??= gameDbService.GetGameAccountCollection();
} }
public async ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType) public async ValueTask<GameAccount?> DetectGameAccountAsync()
{ {
ArgumentNullException.ThrowIfNull(gameAccounts); ArgumentNullException.ThrowIfNull(gameAccounts);
string? registrySdk = RegistryInterop.Get(schemeType); string? registrySdk = RegistryInterop.Get();
if (string.IsNullOrEmpty(registrySdk)) if (!string.IsNullOrEmpty(registrySdk))
{ {
return default; GameAccount? account = null;
try
{
account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
}
if (account is null)
{
LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false);
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false);
if (isOk)
{
account = GameAccount.From(name, registrySdk);
// sync database
await taskContext.SwitchToBackgroundAsync();
await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false);
// sync cache
await taskContext.SwitchToMainThreadAsync();
gameAccounts.Add(account);
}
}
return account;
} }
GameAccount? account = SingleGameAccountOrDefault(gameAccounts, registrySdk); return default;
if (account is null) }
public GameAccount? DetectCurrentGameAccount()
{
ArgumentNullException.ThrowIfNull(gameAccounts);
string? registrySdk = RegistryInterop.Get();
if (!string.IsNullOrEmpty(registrySdk))
{ {
LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false); try
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false);
if (isOk)
{ {
account = GameAccount.From(name, registrySdk, schemeType); return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
// sync database catch (InvalidOperationException ex)
await taskContext.SwitchToBackgroundAsync(); {
await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false); ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
// sync cache
await taskContext.SwitchToMainThreadAsync();
gameAccounts.Add(account);
} }
} }
return account; return null;
}
public GameAccount? DetectCurrentGameAccount(SchemeType schemeType)
{
ArgumentNullException.ThrowIfNull(gameAccounts);
string? registrySdk = RegistryInterop.Get(schemeType);
if (string.IsNullOrEmpty(registrySdk))
{
return default;
}
return SingleGameAccountOrDefault(gameAccounts, registrySdk);
} }
public bool SetGameAccount(GameAccount account) public bool SetGameAccount(GameAccount account)
{ {
return RegistryInterop.Set(account); if (string.IsNullOrEmpty(appOptions.PowerShellPath))
{
ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, default!);
}
return RegistryInterop.Set(account, appOptions.PowerShellPath);
} }
public void AttachGameAccountToUid(GameAccount gameAccount, string uid) public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
@@ -85,12 +106,12 @@ internal sealed partial class GameAccountService : IGameAccountService
public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount) public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
{ {
await taskContext.SwitchToMainThreadAsync();
LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false); LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false);
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false); (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(true);
if (isOk) if (isOk)
{ {
await taskContext.SwitchToMainThreadAsync();
gameAccount.UpdateName(name); gameAccount.UpdateName(name);
// sync database // sync database
@@ -101,24 +122,11 @@ internal sealed partial class GameAccountService : IGameAccountService
public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount) public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
{ {
ArgumentNullException.ThrowIfNull(gameAccounts);
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
ArgumentNullException.ThrowIfNull(gameAccounts);
gameAccounts.Remove(gameAccount); gameAccounts.Remove(gameAccount);
await taskContext.SwitchToBackgroundAsync(); await taskContext.SwitchToBackgroundAsync();
await gameDbService.RemoveGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false); await gameDbService.RemoveGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false);
} }
private static GameAccount? SingleGameAccountOrDefault(ObservableCollection<GameAccount> gameAccounts, string registrySdk)
{
try
{
return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
throw ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
}
}
} }

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game.Account; namespace Snap.Hutao.Service.Game.Account;
@@ -13,9 +12,9 @@ internal interface IGameAccountService
void AttachGameAccountToUid(GameAccount gameAccount, string uid); void AttachGameAccountToUid(GameAccount gameAccount, string uid);
GameAccount? DetectCurrentGameAccount(SchemeType schemeType); GameAccount? DetectCurrentGameAccount();
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType); ValueTask<GameAccount?> DetectGameAccountAsync();
ValueTask ModifyGameAccountAsync(GameAccount gameAccount); ValueTask ModifyGameAccountAsync(GameAccount gameAccount);

View File

@@ -4,7 +4,8 @@
using Microsoft.Win32; using Microsoft.Win32;
using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive; using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@@ -15,21 +16,52 @@ namespace Snap.Hutao.Service.Game.Account;
/// </summary> /// </summary>
internal static class RegistryInterop internal static class RegistryInterop
{ {
private const string ChineseKeyName = @"HKEY_CURRENT_USER\Software\miHoYo\原神"; private const string GenshinPath = @"Software\miHoYo\原神";
private const string OverseaKeyName = @"HKEY_CURRENT_USER\Software\miHoYo\Genshin Impact"; private const string GenshinKey = $@"HKEY_CURRENT_USER\{GenshinPath}";
private const string SdkChineseValueName = "MIHOYOSDK_ADL_PROD_CN_h3123967166"; private const string SdkChineseKey = "MIHOYOSDK_ADL_PROD_CN_h3123967166";
private const string SdkOverseaValueName = "MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810";
public static bool Set(GameAccount? account) /// <summary>
/// 设置键值
/// 需要支持
/// https://learn.microsoft.com/zh-cn/windows/win32/fileio/maximum-file-path-limitation
/// </summary>
/// <param name="account">账户</param>
/// <param name="powerShellPath">PowerShell 路径</param>
/// <returns>账号是否设置</returns>
public static bool Set(GameAccount? account, string powerShellPath)
{ {
if (account is not null) if (account is not null)
{ {
// 存回注册表的字节需要 '\0' 结尾 // 存回注册表的字节需要 '\0' 结尾
byte[] target = [.. Encoding.UTF8.GetBytes(account.MihoyoSDK), 0]; Encoding.UTF8.GetByteCount(account.MihoyoSDK);
(string keyName, string valueName) = GetKeyValueName(account.Type); byte[] tempBytes = Encoding.UTF8.GetBytes(account.MihoyoSDK);
Registry.SetValue(keyName, valueName, target); byte[] target = new byte[tempBytes.Length + 1];
tempBytes.CopyTo(target, 0);
if (Get(account.Type) == account.MihoyoSDK) string base64 = Convert.ToBase64String(target);
string path = $"HKCU:{GenshinPath}";
string command = $"""
-Command "$value = [Convert]::FromBase64String('{base64}'); Set-ItemProperty -Path '{path}' -Name '{SdkChineseKey}' -Value $value -Force;"
""";
ProcessStartInfo startInfo = new()
{
Arguments = command,
WorkingDirectory = Path.GetDirectoryName(powerShellPath),
CreateNoWindow = true,
FileName = powerShellPath,
};
try
{
System.Diagnostics.Process.Start(startInfo)?.WaitForExit();
}
catch (Win32Exception ex)
{
ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropLongPathsDisabled, ex);
}
if (Get() == account.MihoyoSDK)
{ {
return true; return true;
} }
@@ -38,31 +70,24 @@ internal static class RegistryInterop
return false; return false;
} }
public static unsafe string? Get(SchemeType scheme) /// <summary>
/// 在注册表中获取账号信息
/// </summary>
/// <returns>当前注册表中的信息</returns>
public static unsafe string? Get()
{ {
(string keyName, string valueName) = GetKeyValueName(scheme); object? sdk = Registry.GetValue(GenshinKey, SdkChineseKey, Array.Empty<byte>());
object? sdk = Registry.GetValue(keyName, valueName, Array.Empty<byte>());
if (sdk is not byte[] bytes) if (sdk is byte[] bytes)
{ {
return null; fixed (byte* pByte = bytes)
{
// 从注册表获取的字节数组带有 '\0' 结尾,需要舍去
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
return Encoding.UTF8.GetString(span);
}
} }
fixed (byte* pByte = bytes) return null;
{
// 从注册表获取的字节数组带有 '\0' 结尾,需要舍去
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
return Encoding.UTF8.GetString(span);
}
}
private static (string KeyName, string ValueName) GetKeyValueName(SchemeType scheme)
{
return scheme switch
{
SchemeType.ChineseOfficial => (ChineseKeyName, SdkChineseValueName),
SchemeType.Oversea => (OverseaKeyName, SdkOverseaValueName),
_ => throw ThrowHelper.NotSupported($"Invalid account SchemeType: {scheme}"),
};
} }
} }

View File

@@ -34,6 +34,21 @@ internal readonly struct ChannelOptions
/// </summary> /// </summary>
public readonly string? ConfigFilePath; public readonly string? ConfigFilePath;
/// <summary>
/// 构造一个新的多通道
/// </summary>
/// <param name="channel">通道</param>
/// <param name="subChannel">子通道</param>
/// <param name="isOversea">是否为国际服</param>
/// <param name="configFilePath">配置文件路径</param>
public ChannelOptions(string? channel, string? subChannel, bool isOversea, string? configFilePath = null)
{
_ = Enum.TryParse(channel, out Channel);
_ = Enum.TryParse(subChannel, out SubChannel);
IsOversea = isOversea;
ConfigFilePath = configFilePath;
}
public ChannelOptions(ChannelType channel, SubChannelType subChannel, bool isOversea) public ChannelOptions(ChannelType channel, SubChannelType subChannel, bool isOversea)
{ {
Channel = channel; Channel = channel;
@@ -41,33 +56,24 @@ internal readonly struct ChannelOptions
IsOversea = isOversea; IsOversea = isOversea;
} }
public ChannelOptions(string? channel, string? subChannel, bool isOversea) /// <summary>
{ /// 配置文件未找到
_ = Enum.TryParse(channel, out Channel); /// </summary>
_ = Enum.TryParse(subChannel, out SubChannel); /// <param name="isOversea">是否为国际服</param>
IsOversea = isOversea; /// <param name="configFilePath">配置文件期望路径</param>
} /// <returns>选项</returns>
private ChannelOptions(bool isOversea, string? configFilePath)
{
IsOversea = isOversea;
ConfigFilePath = configFilePath;
}
public static ChannelOptions FileNotFound(bool isOversea, string configFilePath) public static ChannelOptions FileNotFound(bool isOversea, string configFilePath)
{ {
return new(isOversea, configFilePath); return new(null, null, isOversea, configFilePath);
} }
/// <inheritdoc/> /// <inheritdoc/>
public override string ToString() public override string ToString()
{ {
return $$""" return $"[ChannelType:{Channel}] [SubChannel:{SubChannel}] [IsOversea: {IsOversea}]";
{ ChannelType: {{Channel}}, SubChannel: {{SubChannel}}, IsOversea: {{IsOversea}}}
""";
} }
// DO NOT DELETE, used in HashSet // DO NOT DELETE used in HashSet
public override int GetHashCode() public override int GetHashCode()
{ {
return HashCode.Combine(Channel, SubChannel, IsOversea); return HashCode.Combine(Channel, SubChannel, IsOversea);

View File

@@ -17,12 +17,9 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
public ChannelOptions GetChannelOptions() public ChannelOptions GetChannelOptions()
{ {
if (!launchOptions.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath)) string gamePath = launchOptions.GamePath;
{ string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName);
throw ThrowHelper.InvalidOperation($"Invalid game path: {gamePath}"); bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase);
}
bool isOversea = LaunchScheme.ExecutableIsOversea(Path.GetFileName(gamePath));
if (!File.Exists(configPath)) if (!File.Exists(configPath))
{ {
@@ -41,10 +38,10 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
public bool SetChannelOptions(LaunchScheme scheme) public bool SetChannelOptions(LaunchScheme scheme)
{ {
if (!launchOptions.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath)) string gamePath = launchOptions.GamePath;
{ string? directory = Path.GetDirectoryName(gamePath);
return false; ArgumentException.ThrowIfNullOrEmpty(directory);
} string configPath = Path.Combine(directory, ConfigFileName);
List<IniElement> elements = default!; List<IniElement> elements = default!;
try try
@@ -73,16 +70,14 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
{ {
if (element is IniParameter parameter) if (element is IniParameter parameter)
{ {
if (parameter.Key is ChannelOptions.ChannelName) if (parameter.Key == "channel")
{ {
changed = parameter.Set(scheme.Channel.ToString("D")) || changed; changed = parameter.Set(scheme.Channel.ToString("D")) || changed;
continue;
} }
if (parameter.Key is ChannelOptions.SubChannelName) if (parameter.Key == "sub_channel")
{ {
changed = parameter.Set(scheme.SubChannel.ToString("D")) || changed; changed = parameter.Set(scheme.SubChannel.ToString("D")) || changed;
continue;
} }
} }
} }

View File

@@ -9,13 +9,38 @@ namespace Snap.Hutao.Service.Game;
[HighQuality] [HighQuality]
internal static class GameConstants internal static class GameConstants
{ {
/// <summary>
/// 设置文件
/// </summary>
public const string ConfigFileName = "config.ini"; public const string ConfigFileName = "config.ini";
/// <summary>
/// 国服文件名
/// </summary>
public const string YuanShenFileName = "YuanShen.exe"; public const string YuanShenFileName = "YuanShen.exe";
public const string YuanShenFileNameUpper = "YUANSHEN.EXE";
/// <summary>
/// 外服文件名
/// </summary>
public const string GenshinImpactFileName = "GenshinImpact.exe"; public const string GenshinImpactFileName = "GenshinImpact.exe";
public const string GenshinImpactFileNameUpper = "GENSHINIMPACT.EXE";
/// <summary>
/// 国服数据文件夹
/// </summary>
public const string YuanShenData = "YuanShen_Data"; public const string YuanShenData = "YuanShen_Data";
/// <summary>
/// 国际服数据文件夹
/// </summary>
public const string GenshinImpactData = "GenshinImpact_Data"; public const string GenshinImpactData = "GenshinImpact_Data";
/// <summary>
/// 国服进程名
/// </summary>
public const string YuanShenProcessName = "YuanShen"; public const string YuanShenProcessName = "YuanShen";
/// <summary>
/// 外服进程名
/// </summary>
public const string GenshinImpactProcessName = "GenshinImpact"; public const string GenshinImpactProcessName = "GenshinImpact";
} }

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.Game.Account; using Snap.Hutao.Service.Game.Account;
using Snap.Hutao.Service.Game.Configuration; using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package; using Snap.Hutao.Service.Game.Package;
@@ -52,15 +51,15 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
} }
/// <inheritdoc/> /// <inheritdoc/>
public ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme) public ValueTask<GameAccount?> DetectGameAccountAsync()
{ {
return gameAccountService.DetectGameAccountAsync(scheme); return gameAccountService.DetectGameAccountAsync();
} }
/// <inheritdoc/> /// <inheritdoc/>
public GameAccount? DetectCurrentGameAccount(SchemeType scheme) public GameAccount? DetectCurrentGameAccount()
{ {
return gameAccountService.DetectCurrentGameAccount(scheme); return gameAccountService.DetectCurrentGameAccount();
} }
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -1,20 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.Scheme;
namespace Snap.Hutao.Service.Game;
internal static class GameServiceFacadeExtension
{
public static GameAccount? DetectCurrentGameAccount(this IGameServiceFacade gameServiceFacade, LaunchScheme scheme)
{
return gameServiceFacade.DetectCurrentGameAccount(scheme.GetSchemeType());
}
public static ValueTask<GameAccount?> DetectGameAccountAsync(this IGameServiceFacade gameServiceFacade, LaunchScheme scheme)
{
return gameServiceFacade.DetectGameAccountAsync(scheme.GetSchemeType());
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.Game.Configuration; using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package; using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Service.Game.Scheme;
@@ -29,7 +28,7 @@ internal interface IGameServiceFacade
/// <param name="uid">uid</param> /// <param name="uid">uid</param>
void AttachGameAccountToUid(GameAccount gameAccount, string uid); void AttachGameAccountToUid(GameAccount gameAccount, string uid);
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme); ValueTask<GameAccount?> DetectGameAccountAsync();
/// <summary> /// <summary>
/// 异步获取游戏路径 /// 异步获取游戏路径
@@ -87,5 +86,9 @@ internal interface IGameServiceFacade
/// <returns>是否更改了ini文件</returns> /// <returns>是否更改了ini文件</returns>
bool SetChannelOptions(LaunchScheme scheme); bool SetChannelOptions(LaunchScheme scheme);
GameAccount? DetectCurrentGameAccount(SchemeType scheme); /// <summary>
/// 检测账号
/// </summary>
/// <returns>账号</returns>
GameAccount? DetectCurrentGameAccount();
} }

View File

@@ -9,25 +9,12 @@ namespace Snap.Hutao.Service.Game;
internal static class LaunchOptionsExtension internal static class LaunchOptionsExtension
{ {
public static bool TryGetGamePathAndGameDirectory(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameDirectory) public static bool TryGetGameFolderAndFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName)
{
gamePath = options.GamePath;
gameDirectory = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameDirectory))
{
return false;
}
return true;
}
public static bool TryGetGameDirectoryAndGameFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameDirectory, [NotNullWhen(true)] out string? gameFileName)
{ {
string gamePath = options.GamePath; string gamePath = options.GamePath;
gameDirectory = Path.GetDirectoryName(gamePath); gameFolder = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameDirectory)) if (string.IsNullOrEmpty(gameFolder))
{ {
gameFileName = default; gameFileName = default;
return false; return false;
@@ -55,18 +42,6 @@ internal static class LaunchOptionsExtension
return true; return true;
} }
public static bool TryGetGamePathAndFilePathByName(this LaunchOptions options, string fileName, out string gamePath, [NotNullWhen(true)] out string? filePath)
{
if (options.TryGetGamePathAndGameDirectory(out gamePath, out string? gameDirectory))
{
filePath = Path.Combine(gameDirectory, fileName);
return true;
}
filePath = default;
return false;
}
public static ImmutableList<GamePathEntry> GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry) public static ImmutableList<GamePathEntry> GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry)
{ {
string gamePath = options.GamePath; string gamePath = options.GamePath;

View File

@@ -4,13 +4,20 @@
namespace Snap.Hutao.Service.Game.Locator; namespace Snap.Hutao.Service.Game.Locator;
[ConstructorGenerated] [ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IGameLocatorFactory))] [Injection(InjectAs.Transient, typeof(IGameLocatorFactory))]
internal sealed partial class GameLocatorFactory : IGameLocatorFactory internal sealed partial class GameLocatorFactory : IGameLocatorFactory
{ {
[SuppressMessage("", "SH301")]
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
public IGameLocator Create(GameLocationSource source) public IGameLocator Create(GameLocationSource source)
{ {
return serviceProvider.GetRequiredKeyedService<IGameLocator>(source); return source switch
{
GameLocationSource.Registry => serviceProvider.GetRequiredService<RegistryLauncherLocator>(),
GameLocationSource.UnityLog => serviceProvider.GetRequiredService<UnityLogGameLocator>(),
GameLocationSource.Manual => serviceProvider.GetRequiredService<ManualGameLocator>(),
_ => throw Must.NeverHappen(),
};
} }
} }

View File

@@ -1,12 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Locator;
internal static class GameLocatorFactoryExtensions
{
public static ValueTask<ValueResult<bool, string>> LocateAsync(this IGameLocatorFactory factory, GameLocationSource source)
{
return factory.Create(source).LocateGamePathAsync();
}
}

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.Service.Game.Locator;
/// </summary> /// </summary>
[HighQuality] [HighQuality]
[ConstructorGenerated] [ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.Manual)] [Injection(InjectAs.Transient)]
internal sealed partial class ManualGameLocator : IGameLocator internal sealed partial class ManualGameLocator : IGameLocator
{ {
private readonly IFileSystemPickerInteraction fileSystemPickerInteraction; private readonly IFileSystemPickerInteraction fileSystemPickerInteraction;
@@ -26,7 +26,7 @@ internal sealed partial class ManualGameLocator : IGameLocator
if (isPickerOk) if (isPickerOk)
{ {
string fileName = System.IO.Path.GetFileName(file); string fileName = System.IO.Path.GetFileName(file);
if (fileName.ToUpperInvariant() is GameConstants.YuanShenFileNameUpper or GameConstants.GenshinImpactFileNameUpper) if (fileName is GameConstants.YuanShenFileName or GameConstants.GenshinImpactFileName)
{ {
return ValueTask.FromResult<ValueResult<bool, string>>(new(true, file)); return ValueTask.FromResult<ValueResult<bool, string>>(new(true, file));
} }

View File

@@ -13,10 +13,9 @@ namespace Snap.Hutao.Service.Game.Locator;
/// </summary> /// </summary>
[HighQuality] [HighQuality]
[ConstructorGenerated] [ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.Registry)] [Injection(InjectAs.Transient)]
internal sealed partial class RegistryLauncherLocator : IGameLocator internal sealed partial class RegistryLauncherLocator : IGameLocator
{ {
private const string RegistryKeyName = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神";
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
/// <inheritdoc/> /// <inheritdoc/>
@@ -30,37 +29,50 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator
{ {
return result; return result;
} }
else
string? path = Path.GetDirectoryName(result.Value);
ArgumentException.ThrowIfNullOrEmpty(path);
string configPath = Path.Combine(path, GameConstants.ConfigFileName);
string? escapedPath;
using (FileStream stream = File.OpenRead(configPath))
{ {
IEnumerable<IniElement> elements = IniSerializer.Deserialize(stream); string? path = Path.GetDirectoryName(result.Value);
escapedPath = elements ArgumentException.ThrowIfNullOrEmpty(path);
.OfType<IniParameter>() string configPath = Path.Combine(path, GameConstants.ConfigFileName);
.FirstOrDefault(p => p.Key == "game_install_path")?.Value; string? escapedPath;
} using (FileStream stream = File.OpenRead(configPath))
{
IEnumerable<IniElement> elements = IniSerializer.Deserialize(stream);
escapedPath = elements
.OfType<IniParameter>()
.FirstOrDefault(p => p.Key == "game_install_path")?.Value;
}
if (!string.IsNullOrEmpty(escapedPath)) if (escapedPath is not null)
{ {
string gamePath = Path.Combine(Unescape(escapedPath), GameConstants.YuanShenFileName); string gamePath = Path.Combine(Unescape(escapedPath), GameConstants.YuanShenFileName);
return new(true, gamePath); return new(true, gamePath);
}
} }
return new(false, string.Empty); return new(false, string.Empty);
} }
private static ValueResult<bool, string> LocateInternal(string valueName) private static ValueResult<bool, string> LocateInternal(string key)
{ {
if (Registry.GetValue(RegistryKeyName, valueName, null) is string path) using (RegistryKey? uninstallKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神"))
{ {
return new(true, path); if (uninstallKey is not null)
{
if (uninstallKey.GetValue(key) is string path)
{
return new(true, path);
}
else
{
return new(false, default!);
}
}
else
{
return new(false, default!);
}
} }
return new(false, default!);
} }
private static string Unescape(string str) private static string Unescape(string str)

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Service.Game.Locator;
/// </summary> /// </summary>
[HighQuality] [HighQuality]
[ConstructorGenerated] [ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.UnityLog)] [Injection(InjectAs.Transient)]
internal sealed partial class UnityLogGameLocator : IGameLocator internal sealed partial class UnityLogGameLocator : IGameLocator
{ {
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;

View File

@@ -21,7 +21,7 @@ internal sealed partial class GamePackageService : IGamePackageService
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress) public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
{ {
if (!launchOptions.TryGetGameDirectoryAndGameFileName(out string? gameFolder, out string? gameFileName)) if (!launchOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName))
{ {
return false; return false;
} }
@@ -47,7 +47,8 @@ internal sealed partial class GamePackageService : IGamePackageService
if (!launchScheme.ExecutableMatches(gameFileName)) if (!launchScheme.ExecutableMatches(gameFileName))
{ {
// We can't start the game when we failed to convert game // We can't start the game
// when we failed to convert game
if (!await packageConverter.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress).ConfigureAwait(false)) if (!await packageConverter.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress).ConfigureAwait(false))
{ {
return false; return false;
@@ -66,13 +67,6 @@ internal sealed partial class GamePackageService : IGamePackageService
private static bool CheckDirectoryPermissions(string folder) private static bool CheckDirectoryPermissions(string folder)
{ {
// Program Files has special permissions limitation.
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
if (folder.StartsWith(programFiles, StringComparison.OrdinalIgnoreCase))
{
return false;
}
try try
{ {
string tempFilePath = Path.Combine(folder, $"{Guid.NewGuid():N}.tmp"); string tempFilePath = Path.Combine(folder, $"{Guid.NewGuid():N}.tmp");

View File

@@ -10,12 +10,12 @@ namespace Snap.Hutao.Service.Game.Package;
/// </summary> /// </summary>
[HighQuality] [HighQuality]
[DebuggerDisplay("Action:{Type} Target:{Target} Cache:{Cache}")] [DebuggerDisplay("Action:{Type} Target:{Target} Cache:{Cache}")]
internal readonly struct PackageItemOperationInfo internal readonly struct ItemOperationInfo
{ {
/// <summary> /// <summary>
/// 操作的类型 /// 操作的类型
/// </summary> /// </summary>
public readonly PackageItemOperationType Type; public readonly ItemOperationType Type;
/// <summary> /// <summary>
/// 目标文件 /// 目标文件
@@ -33,7 +33,7 @@ internal readonly struct PackageItemOperationInfo
/// <param name="type">操作类型</param> /// <param name="type">操作类型</param>
/// <param name="remote">远程</param> /// <param name="remote">远程</param>
/// <param name="local">本地</param> /// <param name="local">本地</param>
public PackageItemOperationInfo(PackageItemOperationType type, VersionItem remote, VersionItem local) public ItemOperationInfo(ItemOperationType type, VersionItem remote, VersionItem local)
{ {
Type = type; Type = type;
Remote = remote; Remote = remote;

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.Game.Package;
/// 包文件操作的类型 /// 包文件操作的类型
/// </summary> /// </summary>
[HighQuality] [HighQuality]
internal enum PackageItemOperationType internal enum ItemOperationType
{ {
/// <summary> /// <summary>
/// 需要备份 /// 需要备份

View File

@@ -6,7 +6,7 @@ using static Snap.Hutao.Service.Game.GameConstants;
namespace Snap.Hutao.Service.Game.Package; namespace Snap.Hutao.Service.Game.Package;
internal readonly struct PackageConverterFileSystemContext internal readonly struct PackageConvertContext
{ {
public readonly string GameFolder; public readonly string GameFolder;
public readonly string ServerCacheFolder; public readonly string ServerCacheFolder;
@@ -22,7 +22,7 @@ internal readonly struct PackageConverterFileSystemContext
public readonly string ScatteredFilesUrl; public readonly string ScatteredFilesUrl;
public readonly string PkgVersionUrl; public readonly string PkgVersionUrl;
public PackageConverterFileSystemContext(bool isTargetOversea, string dataFolder, string gameFolder, string scatteredFilesUrl) public PackageConvertContext(bool isTargetOversea, string dataFolder, string gameFolder, string scatteredFilesUrl)
{ {
GameFolder = gameFolder; GameFolder = gameFolder;
ServerCacheFolder = Path.Combine(dataFolder, "ServerCache"); ServerCacheFolder = Path.Combine(dataFolder, "ServerCache");
@@ -37,8 +37,7 @@ internal readonly struct PackageConverterFileSystemContext
? (YuanShenData, GenshinImpactData) ? (YuanShenData, GenshinImpactData)
: (GenshinImpactData, YuanShenData); : (GenshinImpactData, YuanShenData);
FromDataFolder = Path.Combine(GameFolder, FromDataFolderName); (FromDataFolder, ToDataFolder) = (Path.Combine(GameFolder, FromDataFolderName), Path.Combine(GameFolder, ToDataFolderName));
ToDataFolder = Path.Combine(GameFolder, ToDataFolderName);
ScatteredFilesUrl = scatteredFilesUrl; ScatteredFilesUrl = scatteredFilesUrl;
PkgVersionUrl = $"{scatteredFilesUrl}/pkg_version"; PkgVersionUrl = $"{scatteredFilesUrl}/pkg_version";

View File

@@ -15,7 +15,6 @@ using System.IO.Compression;
using System.Net.Http; using System.Net.Http;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using static Snap.Hutao.Service.Game.GameConstants; using static Snap.Hutao.Service.Game.GameConstants;
using RelativePathVersionItemDictionary = System.Collections.Generic.Dictionary<string, Snap.Hutao.Service.Game.Package.VersionItem>;
namespace Snap.Hutao.Service.Game.Package; namespace Snap.Hutao.Service.Game.Package;
@@ -59,15 +58,15 @@ internal sealed partial class PackageConverter
string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath; string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath;
string pkgVersionUrl = $"{scatteredFilesUrl}/{PackageVersion}"; string pkgVersionUrl = $"{scatteredFilesUrl}/{PackageVersion}";
PackageConverterFileSystemContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl); PackageConvertContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl);
// Step 1 // Step 1
progress.Report(new(SH.ServiceGamePackageRequestPackageVerion)); progress.Report(new(SH.ServiceGamePackageRequestPackageVerion));
RelativePathVersionItemDictionary remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false); Dictionary<string, VersionItem> remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false);
RelativePathVersionItemDictionary localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false); Dictionary<string, VersionItem> localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false);
// Step 2 // Step 2
List<PackageItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList(); List<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList();
diffOperations.SortBy(i => i.Type); diffOperations.SortBy(i => i.Type);
// Step 3 // Step 3
@@ -117,16 +116,16 @@ internal sealed partial class PackageConverter
} }
} }
private static IEnumerable<PackageItemOperationInfo> GetItemOperationInfos(RelativePathVersionItemDictionary remote, RelativePathVersionItemDictionary local) private static IEnumerable<ItemOperationInfo> GetItemOperationInfos(Dictionary<string, VersionItem> remote, Dictionary<string, VersionItem> local)
{ {
foreach ((string remoteName, VersionItem remoteItem) in remote) foreach ((string remoteName, VersionItem remoteItem) in remote)
{ {
if (local.TryGetValue(remoteName, out VersionItem? localItem)) if (local.TryGetValue(remoteName, out VersionItem? localItem))
{ {
if (!(remoteItem.FileSize == localItem.FileSize && remoteItem.Md5.Equals(localItem.Md5, StringComparison.OrdinalIgnoreCase))) if (!remoteItem.Md5.Equals(localItem.Md5, StringComparison.OrdinalIgnoreCase))
{ {
// 本地发现了同名且不同 MD5 的项,需要替换为服务器上的项 // 本地发现了同名且不同 MD5 的项,需要替换为服务器上的项
yield return new(PackageItemOperationType.Replace, remoteItem, localItem); yield return new(ItemOperationType.Replace, remoteItem, localItem);
} }
// 同名同MD5跳过 // 同名同MD5跳过
@@ -135,22 +134,22 @@ internal sealed partial class PackageConverter
else else
{ {
// 本地没有发现同名项 // 本地没有发现同名项
yield return new(PackageItemOperationType.Add, remoteItem, remoteItem); yield return new(ItemOperationType.Add, remoteItem, remoteItem);
} }
} }
foreach ((_, VersionItem localItem) in local) foreach ((_, VersionItem localItem) in local)
{ {
yield return new(PackageItemOperationType.Backup, localItem, localItem); yield return new(ItemOperationType.Backup, localItem, localItem);
} }
} }
[GeneratedRegex("^(?:YuanShen_Data|GenshinImpact_Data)(?=/)")] [GeneratedRegex("^(?:YuanShen_Data|GenshinImpact_Data)(?=/)")]
private static partial Regex DataFolderRegex(); private static partial Regex DataFolderRegex();
private async ValueTask<RelativePathVersionItemDictionary> GetVersionItemsAsync(Stream stream) private async ValueTask<Dictionary<string, VersionItem>> GetVersionItemsAsync(Stream stream)
{ {
RelativePathVersionItemDictionary results = []; Dictionary<string, VersionItem> results = [];
using (StreamReader reader = new(stream)) using (StreamReader reader = new(stream))
{ {
while (await reader.ReadLineAsync().ConfigureAwait(false) is { Length: > 0 } row) while (await reader.ReadLineAsync().ConfigureAwait(false) is { Length: > 0 } row)
@@ -165,7 +164,7 @@ internal sealed partial class PackageConverter
return results; return results;
} }
private async ValueTask<RelativePathVersionItemDictionary> GetRemoteItemsAsync(string pkgVersionUrl) private async ValueTask<Dictionary<string, VersionItem>> GetRemoteItemsAsync(string pkgVersionUrl)
{ {
try try
{ {
@@ -180,7 +179,7 @@ internal sealed partial class PackageConverter
} }
} }
private async ValueTask<RelativePathVersionItemDictionary> GetLocalItemsAsync(string gameFolder) private async ValueTask<Dictionary<string, VersionItem>> GetLocalItemsAsync(string gameFolder)
{ {
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, PackageVersion))) using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, PackageVersion)))
{ {
@@ -188,23 +187,23 @@ internal sealed partial class PackageConverter
} }
} }
private async ValueTask PrepareCacheFilesAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress) private async ValueTask PrepareCacheFilesAsync(List<ItemOperationInfo> operations, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
{ {
foreach (PackageItemOperationInfo info in operations) foreach (ItemOperationInfo info in operations)
{ {
switch (info.Type) switch (info.Type)
{ {
case PackageItemOperationType.Backup: case ItemOperationType.Backup:
continue; continue;
case PackageItemOperationType.Replace: case ItemOperationType.Replace:
case PackageItemOperationType.Add: case ItemOperationType.Add:
await SkipOrDownloadAsync(info, context, progress).ConfigureAwait(false); await SkipOrDownloadAsync(info, context, progress).ConfigureAwait(false);
break; break;
} }
} }
} }
private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress) private async ValueTask SkipOrDownloadAsync(ItemOperationInfo info, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
{ {
// 还原正确的远程地址 // 还原正确的远程地址
string remoteName = string.Format(CultureInfo.CurrentCulture, info.Remote.RelativePath, context.ToDataFolderName); string remoteName = string.Format(CultureInfo.CurrentCulture, info.Remote.RelativePath, context.ToDataFolderName);
@@ -258,16 +257,16 @@ internal sealed partial class PackageConverter
} }
} }
private async ValueTask<bool> ReplaceGameResourceAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress) private async ValueTask<bool> ReplaceGameResourceAsync(List<ItemOperationInfo> operations, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
{ {
// 执行下载与移动操作 // 执行下载与移动操作
foreach (PackageItemOperationInfo info in operations) foreach (ItemOperationInfo info in operations)
{ {
(bool moveToBackup, bool moveToTarget) = info.Type switch (bool moveToBackup, bool moveToTarget) = info.Type switch
{ {
PackageItemOperationType.Backup => (true, false), ItemOperationType.Backup => (true, false),
PackageItemOperationType.Replace => (true, true), ItemOperationType.Replace => (true, true),
PackageItemOperationType.Add => (false, true), ItemOperationType.Add => (false, true),
_ => (false, false), _ => (false, false),
}; };
@@ -322,7 +321,7 @@ internal sealed partial class PackageConverter
return true; return true;
} }
private async ValueTask ReplacePackageVersionFilesAsync(PackageConverterFileSystemContext context) private async ValueTask ReplacePackageVersionFilesAsync(PackageConvertContext context)
{ {
foreach (string versionFilePath in Directory.EnumerateFiles(context.GameFolder, "*pkg_version")) foreach (string versionFilePath in Directory.EnumerateFiles(context.GameFolder, "*pkg_version"))
{ {

View File

@@ -2,13 +2,14 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.Common; using CommunityToolkit.Common;
using Snap.Hutao.Core.Abstraction;
namespace Snap.Hutao.Service.Game.Package; namespace Snap.Hutao.Service.Game.Package;
/// <summary> /// <summary>
/// 包更新状态 /// 包更新状态
/// </summary> /// </summary>
internal sealed class PackageReplaceStatus internal sealed class PackageReplaceStatus : ICloneable<PackageReplaceStatus>
{ {
/// <summary> /// <summary>
/// 构造一个新的包更新状态 /// 构造一个新的包更新状态
@@ -33,6 +34,10 @@ internal sealed class PackageReplaceStatus
Description = $"{Converters.ToFileSizeString(bytesRead)}/{Converters.ToFileSizeString(totalBytes)}"; Description = $"{Converters.ToFileSizeString(bytesRead)}/{Converters.ToFileSizeString(totalBytes)}";
} }
private PackageReplaceStatus()
{
}
public string Name { get; set; } = default!; public string Name { get; set; } = default!;
/// <summary> /// <summary>
@@ -49,4 +54,19 @@ internal sealed class PackageReplaceStatus
/// 是否有进度 /// 是否有进度
/// </summary> /// </summary>
public bool IsIndeterminate { get => Percent < 0; } public bool IsIndeterminate { get => Percent < 0; }
/// <summary>
/// 克隆
/// </summary>
/// <returns>克隆的实例</returns>
public PackageReplaceStatus Clone()
{
// 进度需要在主线程上创建
return new()
{
Name = Name,
Description = Description,
Percent = Percent,
};
}
} }

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Service.Game.PathAbstraction;
[Injection(InjectAs.Singleton, typeof(IGamePathService))] [Injection(InjectAs.Singleton, typeof(IGamePathService))]
internal sealed partial class GamePathService : IGamePathService internal sealed partial class GamePathService : IGamePathService
{ {
private readonly IGameLocatorFactory gameLocatorFactory; private readonly IServiceProvider serviceProvider;
private readonly LaunchOptions launchOptions; private readonly LaunchOptions launchOptions;
public async ValueTask<ValueResult<bool, string>> SilentGetGamePathAsync() public async ValueTask<ValueResult<bool, string>> SilentGetGamePathAsync()
@@ -17,16 +17,24 @@ internal sealed partial class GamePathService : IGamePathService
// Cannot find in setting // Cannot find in setting
if (string.IsNullOrEmpty(launchOptions.GamePath)) if (string.IsNullOrEmpty(launchOptions.GamePath))
{ {
IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService<IGameLocatorFactory>();
bool isOk; bool isOk;
string path; string path;
// Try locate by unity log // Try locate by unity log
(isOk, path) = await gameLocatorFactory.LocateAsync(GameLocationSource.UnityLog).ConfigureAwait(false); (isOk, path) = await locatorFactory
.Create(GameLocationSource.UnityLog)
.LocateGamePathAsync()
.ConfigureAwait(false);
if (!isOk) if (!isOk)
{ {
// Try locate by registry // Try locate by registry
(isOk, path) = await gameLocatorFactory.LocateAsync(GameLocationSource.Registry).ConfigureAwait(false); (isOk, path) = await locatorFactory
.Create(GameLocationSource.Registry)
.LocateGamePathAsync()
.ConfigureAwait(false);
} }
if (isOk) if (isOk)
@@ -40,11 +48,13 @@ internal sealed partial class GamePathService : IGamePathService
} }
} }
if (string.IsNullOrEmpty(launchOptions.GamePath)) if (!string.IsNullOrEmpty(launchOptions.GamePath))
{
return new(true, launchOptions.GamePath);
}
else
{ {
return new(false, default!); return new(false, default!);
} }
return new(true, launchOptions.GamePath);
} }
} }

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core; using Snap.Hutao.Core;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Service.Discord; using Snap.Hutao.Service.Discord;
using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Game.Unlocker; using Snap.Hutao.Service.Game.Unlocker;
@@ -19,7 +18,6 @@ namespace Snap.Hutao.Service.Game.Process;
internal sealed partial class GameProcessService : IGameProcessService internal sealed partial class GameProcessService : IGameProcessService
{ {
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
private readonly IProgressFactory progressFactory;
private readonly IDiscordService discordService; private readonly IDiscordService discordService;
private readonly RuntimeOptions runtimeOptions; private readonly RuntimeOptions runtimeOptions;
private readonly LaunchOptions launchOptions; private readonly LaunchOptions launchOptions;
@@ -111,13 +109,13 @@ internal sealed partial class GameProcessService : IGameProcessService
// https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html // https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html
// https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html // https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html
commandLine = new CommandLineBuilder() commandLine = new CommandLineBuilder()
.AppendIf(launchOptions.IsBorderless, "-popupwindow") .AppendIf("-popupwindow", launchOptions.IsBorderless)
.AppendIf(launchOptions.IsExclusive, "-window-mode", "exclusive") .AppendIf("-window-mode", launchOptions.IsExclusive, "exclusive")
.Append("-screen-fullscreen", launchOptions.IsFullScreen ? 1 : 0) .Append("-screen-fullscreen", launchOptions.IsFullScreen ? 1 : 0)
.AppendIf(launchOptions.IsScreenWidthEnabled, "-screen-width", launchOptions.ScreenWidth) .AppendIf("-screen-width", launchOptions.IsScreenWidthEnabled, launchOptions.ScreenWidth)
.AppendIf(launchOptions.IsScreenHeightEnabled, "-screen-height", launchOptions.ScreenHeight) .AppendIf("-screen-height", launchOptions.IsScreenHeightEnabled, launchOptions.ScreenHeight)
.AppendIf(launchOptions.IsMonitorEnabled, "-monitor", launchOptions.Monitor.Value) .AppendIf("-monitor", launchOptions.IsMonitorEnabled, launchOptions.Monitor.Value)
.AppendIf(launchOptions.IsUseCloudThirdPartyMobile, "-platform_type CLOUD_THIRD_PARTY_MOBILE") .AppendIf("-platform_type CLOUD_THIRD_PARTY_MOBILE", launchOptions.IsUseCloudThirdPartyMobile)
.ToString(); .ToString();
} }
@@ -140,7 +138,7 @@ internal sealed partial class GameProcessService : IGameProcessService
IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game); IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game);
#pragma warning restore CA1859 #pragma warning restore CA1859
UnlockTimingOptions options = new(100, 20000, 3000); UnlockTimingOptions options = new(100, 20000, 3000);
IProgress<UnlockerStatus> lockerProgress = progressFactory.CreateForMainThread<UnlockerStatus>(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus))); Progress<UnlockerStatus> lockerProgress = new(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus)));
return unlocker.UnlockAsync(options, lockerProgress, token); return unlocker.UnlockAsync(options, lockerProgress, token);
} }

View File

@@ -59,11 +59,11 @@ internal class LaunchScheme : IEquatable<ChannelOptions>
public static bool ExecutableIsOversea(string gameFileName) public static bool ExecutableIsOversea(string gameFileName)
{ {
return gameFileName.ToUpperInvariant() switch return gameFileName switch
{ {
GameConstants.GenshinImpactFileNameUpper => true, GameConstants.GenshinImpactFileName => true,
GameConstants.YuanShenFileNameUpper => false, GameConstants.YuanShenFileName => false,
_ => throw Requires.Fail("Invalid game executable file name{0}", gameFileName), _ => throw Requires.Fail("无效的游戏可执行文件名称{0}", gameFileName),
}; };
} }

View File

@@ -1,20 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Model.Intrinsic;
namespace Snap.Hutao.Service.Game.Scheme;
internal static class LaunchSchemeExtension
{
public static SchemeType GetSchemeType(this LaunchScheme scheme)
{
return (scheme.Channel, scheme.IsOversea) switch
{
(ChannelType.Bili, false) => SchemeType.ChineseBilibili,
(_, false) => SchemeType.ChineseOfficial,
(_, true) => SchemeType.Oversea,
};
}
}

View File

@@ -228,7 +228,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
nuint rip = localMemoryUserAssemblyAddress + (uint)offset; nuint rip = localMemoryUserAssemblyAddress + (uint)offset;
rip += 5U; rip += 5U;
rip += (nuint)(*(int*)(rip + 2U) + 6); rip += (nuint)(*(int*)(rip + 2) + 6);
nuint address = userAssembly.Address + (rip - localMemoryUserAssemblyAddress); nuint address = userAssembly.Address + (rip - localMemoryUserAssemblyAddress);
@@ -236,8 +236,6 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
SpinWait.SpinUntil(() => UnsafeReadProcessMemory(gameProcess, address, out ptr) && ptr != 0); SpinWait.SpinUntil(() => UnsafeReadProcessMemory(gameProcess, address, out ptr) && ptr != 0);
rip = ptr - unityPlayer.Address + localMemoryUnityPlayerAddress; rip = ptr - unityPlayer.Address + localMemoryUnityPlayerAddress;
// CALL or JMP
while (*(byte*)rip == 0xE8 || *(byte*)rip == 0xE9) while (*(byte*)rip == 0xE8 || *(byte*)rip == 0xE9)
{ {
rip += (nuint)(*(int*)(rip + 1) + 5); rip += (nuint)(*(int*)(rip + 1) + 5);

View File

@@ -9,5 +9,5 @@ internal interface IUpdateService
{ {
ValueTask<bool> CheckForUpdateAndDownloadAsync(IProgress<UpdateStatus> progress, CancellationToken token = default); ValueTask<bool> CheckForUpdateAndDownloadAsync(IProgress<UpdateStatus> progress, CancellationToken token = default);
ValueTask LaunchUpdaterAsync(); void LaunchInstaller();
} }

View File

@@ -4,14 +4,12 @@
using Snap.Hutao.Core; using Snap.Hutao.Core;
using Snap.Hutao.Core.IO.Hashing; using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Core.IO.Http.Sharding; using Snap.Hutao.Core.IO.Http.Sharding;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hutao; using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.Response; using Snap.Hutao.Web.Hutao.Response;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using Windows.Storage;
namespace Snap.Hutao.Service.Update; namespace Snap.Hutao.Service.Update;
@@ -19,8 +17,6 @@ namespace Snap.Hutao.Service.Update;
[Injection(InjectAs.Singleton, typeof(IUpdateService))] [Injection(InjectAs.Singleton, typeof(IUpdateService))]
internal sealed partial class UpdateService : IUpdateService internal sealed partial class UpdateService : IUpdateService
{ {
private const string UpdaterFilename = "Snap.Hutao.Deployment.exe";
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
public async ValueTask<bool> CheckForUpdateAndDownloadAsync(IProgress<UpdateStatus> progress, CancellationToken token = default) public async ValueTask<bool> CheckForUpdateAndDownloadAsync(IProgress<UpdateStatus> progress, CancellationToken token = default)
@@ -41,22 +37,19 @@ internal sealed partial class UpdateService : IUpdateService
HutaoVersionInformation versionInformation = response.Data; HutaoVersionInformation versionInformation = response.Data;
string msixPath = GetUpdatePackagePath(); string msixPath = GetUpdatePackagePath();
if (!LocalSetting.Get(SettingKeys.OverrideUpdateVersionComparison, false)) if (scope.ServiceProvider.GetRequiredService<RuntimeOptions>().Version >= versionInformation.Version)
{ {
if (scope.ServiceProvider.GetRequiredService<RuntimeOptions>().Version >= versionInformation.Version) if (File.Exists(msixPath))
{ {
if (File.Exists(msixPath)) File.Delete(msixPath);
{
File.Delete(msixPath);
}
return false;
} }
return false;
} }
progress.Report(new(versionInformation.Version.ToString(), 0, 0)); progress.Report(new(versionInformation.Version.ToString(), 0, 0));
if (versionInformation.Sha256 is not { Length: > 0 } sha256) if (versionInformation.Sha256 is not { } sha256)
{ {
return false; return false;
} }
@@ -70,27 +63,12 @@ internal sealed partial class UpdateService : IUpdateService
} }
} }
public async ValueTask LaunchUpdaterAsync() public void LaunchInstaller()
{ {
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
string updaterTargetPath = runtimeOptions.GetDataFolderUpdateCacheFolderFile(UpdaterFilename);
Uri updaterSourceUri = $"ms-appx:///{UpdaterFilename}".ToUri();
StorageFile updaterFile = await StorageFile.GetFileFromApplicationUriAsync(updaterSourceUri);
await updaterFile.OverwriteCopyAsync(updaterTargetPath).ConfigureAwait(false);
string commandLine = new CommandLineBuilder()
.Append("--package-path", GetUpdatePackagePath(runtimeOptions))
.Append("--family-name", runtimeOptions.FamilyName)
.Append("--update-behavior", true)
.ToString();
Process.Start(new ProcessStartInfo() Process.Start(new ProcessStartInfo()
{ {
Arguments = commandLine,
WindowStyle = ProcessWindowStyle.Minimized,
FileName = updaterTargetPath,
UseShellExecute = true, UseShellExecute = true,
FileName = GetUpdatePackagePath(),
}); });
} }
@@ -100,10 +78,12 @@ internal sealed partial class UpdateService : IUpdateService
return string.Equals(localHash, remoteHash, StringComparison.OrdinalIgnoreCase); return string.Equals(localHash, remoteHash, StringComparison.OrdinalIgnoreCase);
} }
private string GetUpdatePackagePath(RuntimeOptions? runtimeOptions = default) private string GetUpdatePackagePath()
{ {
runtimeOptions ??= serviceProvider.GetRequiredService<RuntimeOptions>(); string dataFolder = serviceProvider.GetRequiredService<RuntimeOptions>().DataFolder;
return runtimeOptions.GetDataFolderUpdateCacheFolderFile("Snap.Hutao.msix"); string directory = Path.Combine(dataFolder, "UpdateCache");
Directory.CreateDirectory(directory);
return Path.Combine(directory, "Snap.Hutao.msix");
} }
private async ValueTask<bool> DownloadUpdatePackageAsync(HutaoVersionInformation versionInformation, string filePath, IProgress<UpdateStatus> progress, CancellationToken token = default) private async ValueTask<bool> DownloadUpdatePackageAsync(HutaoVersionInformation versionInformation, string filePath, IProgress<UpdateStatus> progress, CancellationToken token = default)

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.Service.User;
[ConstructorGenerated] [ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IUserCollectionService))] [Injection(InjectAs.Singleton, typeof(IUserCollectionService))]
internal sealed partial class UserCollectionService : IUserCollectionService, IDisposable internal sealed partial class UserCollectionService : IUserCollectionService
{ {
private readonly ScopedDbCurrent<BindingUser, Model.Entity.User, UserChangedMessage> dbCurrent; private readonly ScopedDbCurrent<BindingUser, Model.Entity.User, UserChangedMessage> dbCurrent;
private readonly IUserInitializationService userInitializationService; private readonly IUserInitializationService userInitializationService;
@@ -22,7 +22,7 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
private readonly IMessenger messenger; private readonly IMessenger messenger;
private readonly SemaphoreSlim throttler = new(1); private readonly Throttler throttler = new();
private ObservableCollection<BindingUser>? userCollection; private ObservableCollection<BindingUser>? userCollection;
private Dictionary<string, BindingUser>? midUserMap; private Dictionary<string, BindingUser>? midUserMap;
@@ -38,9 +38,7 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
public async ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync() public async ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync()
{ {
// Force run in background thread, otherwise will cause reentrance using (await throttler.ThrottleAsync().ConfigureAwait(false))
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
using (await throttler.EnterAsync().ConfigureAwait(false))
{ {
if (userCollection is null) if (userCollection is null)
{ {
@@ -133,7 +131,17 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
return default; return default;
} }
return uidUserGameRoleMap.GetValueOrDefault(uid); try
{
return uidUserGameRoleMap[uid];
}
catch (InvalidOperationException)
{
// Sequence contains more than one matching element
// TODO: return a specialize UserGameRole to indicate error
}
return default;
} }
public bool TryGetUserByMid(string mid, [NotNullWhen(true)] out BindingUser? user) public bool TryGetUserByMid(string mid, [NotNullWhen(true)] out BindingUser? user)
@@ -178,9 +186,4 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
ArgumentNullException.ThrowIfNull(newUser.UserInfo); ArgumentNullException.ThrowIfNull(newUser.UserInfo);
return new(UserOptionResult.Added, newUser.UserInfo.Uid); return new(UserOptionResult.Added, newUser.UserInfo.Uid);
} }
public void Dispose()
{
throttler.Dispose();
}
} }

View File

@@ -204,10 +204,8 @@
<AdditionalFiles Include="stylecop.json" /> <AdditionalFiles Include="stylecop.json" />
<AdditionalFiles Include="Resource\Localization\SH.resx" /> <AdditionalFiles Include="Resource\Localization\SH.resx" />
<AdditionalFiles Include="Resource\Localization\SH.en.resx" /> <AdditionalFiles Include="Resource\Localization\SH.en.resx" />
<AdditionalFiles Include="Resource\Localization\SH.id.resx" />
<AdditionalFiles Include="Resource\Localization\SH.ja.resx" /> <AdditionalFiles Include="Resource\Localization\SH.ja.resx" />
<AdditionalFiles Include="Resource\Localization\SH.ko.resx" /> <AdditionalFiles Include="Resource\Localization\SH.ko.resx" />
<AdditionalFiles Include="Resource\Localization\SH.ru.resx" />
<AdditionalFiles Include="Resource\Localization\SH.zh-Hant.resx" /> <AdditionalFiles Include="Resource\Localization\SH.zh-Hant.resx" />
</ItemGroup> </ItemGroup>
@@ -303,19 +301,11 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.1.1" /> <PackageReference Include="Microsoft.Graphics.Win2D" Version="1.1.1" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.8.8" /> <PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.8.8" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231115000" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231115000" />
<PackageReference Include="QRCoder" Version="1.4.3" /> <PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="Snap.Discord.GameSDK" Version="1.5.0" /> <PackageReference Include="Snap.Discord.GameSDK" Version="1.5.0" />
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="1.9.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Snap.Hutao.SourceGeneration" Version="1.0.3"> <PackageReference Include="Snap.Hutao.SourceGeneration" Version="1.0.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -67,7 +67,7 @@
VerticalAlignment="Bottom"> VerticalAlignment="Bottom">
<ComboBox <ComboBox
DisplayMemberPath="Name" DisplayMemberPath="Name"
ItemsSource="{Binding GameAccountsView}" ItemsSource="{Binding GameAccounts}"
PlaceholderText="{shcm:ResourceString Name=ViewCardLaunchGameSelectAccountPlaceholder}" PlaceholderText="{shcm:ResourceString Name=ViewCardLaunchGameSelectAccountPlaceholder}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/> SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl> </shc:SizeRestrictedContentControl>

View File

@@ -76,15 +76,16 @@ internal partial class WebViewer : UserControl, IRecipient<UserChangedMessage>
private async ValueTask InitializeAsync() private async ValueTask InitializeAsync()
{ {
if (!isInitializingOrInitialized) if (isInitializingOrInitialized)
{ {
isInitializingOrInitialized = true; return;
await WebView.EnsureCoreWebView2Async();
WebView.CoreWebView2.DisableDevToolsForReleaseBuild();
WebView.CoreWebView2.DocumentTitleChanged += documentTitleChangedEventHander;
} }
isInitializingOrInitialized = true;
await WebView.EnsureCoreWebView2Async();
WebView.CoreWebView2.DisableDevToolsForReleaseBuild();
WebView.CoreWebView2.DocumentTitleChanged += documentTitleChangedEventHander;
RefreshWebview2Content(); RefreshWebview2Content();
} }
@@ -127,9 +128,6 @@ internal partial class WebViewer : UserControl, IRecipient<UserChangedMessage>
string source = SourceProvider.GetSource(userAndUid); string source = SourceProvider.GetSource(userAndUid);
if (!string.IsNullOrEmpty(source)) if (!string.IsNullOrEmpty(source))
{ {
CoreWebView2Navigator navigator = new(coreWebView2);
await navigator.NavigateAsync("about:blank").ConfigureAwait(true);
try try
{ {
await coreWebView2.Profile.ClearBrowsingDataAsync(); await coreWebView2.Profile.ClearBrowsingDataAsync();
@@ -140,6 +138,9 @@ internal partial class WebViewer : UserControl, IRecipient<UserChangedMessage>
await coreWebView2.DeleteCookiesAsync(userAndUid.IsOversea).ConfigureAwait(true); await coreWebView2.DeleteCookiesAsync(userAndUid.IsOversea).ConfigureAwait(true);
} }
CoreWebView2Navigator navigator = new(coreWebView2);
await navigator.NavigateAsync("about:blank").ConfigureAwait(true);
coreWebView2 coreWebView2
.SetCookie(user.CookieToken, user.LToken, userAndUid.IsOversea) .SetCookie(user.CookieToken, user.LToken, userAndUid.IsOversea)
.SetMobileUserAgent(userAndUid.IsOversea); .SetMobileUserAgent(userAndUid.IsOversea);

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.WinUI.Converters; using CommunityToolkit.WinUI.Converters;
using Snap.Hutao.Control;
namespace Snap.Hutao.View.Converter; namespace Snap.Hutao.View.Converter;

View File

@@ -13,7 +13,6 @@
xmlns:shcb="using:Snap.Hutao.Control.Behavior" xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shccs="using:Snap.Hutao.Control.Collection.Selector" xmlns:shccs="using:Snap.Hutao.Control.Collection.Selector"
xmlns:shch="using:Snap.Hutao.Control.Helper" xmlns:shch="using:Snap.Hutao.Control.Helper"
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shcm="using:Snap.Hutao.Control.Markup" xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shvc="using:Snap.Hutao.View.Control" xmlns:shvc="using:Snap.Hutao.View.Control"
xmlns:shvg="using:Snap.Hutao.ViewModel.Game" xmlns:shvg="using:Snap.Hutao.ViewModel.Game"
@@ -198,7 +197,7 @@
<Border Style="{StaticResource BorderCardStyle}"> <Border Style="{StaticResource BorderCardStyle}">
<ListView <ListView
ItemTemplate="{StaticResource GameAccountListTemplate}" ItemTemplate="{StaticResource GameAccountListTemplate}"
ItemsSource="{Binding GameAccountsView}" ItemsSource="{Binding GameAccounts}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/> SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
</Border> </Border>
@@ -347,20 +346,10 @@
</Grid> </Grid>
<Grid Visibility="{Binding GamePathSelectedAndValid, Converter={StaticResource BoolToVisibilityRevertConverter}}"> <Grid Visibility="{Binding GamePathSelectedAndValid, Converter={StaticResource BoolToVisibilityRevertConverter}}">
<StackPanel <StackPanel
Margin="128,0" MaxWidth="600"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Spacing="3"> Spacing="3">
<shci:CachedImage
Width="120"
Height="120"
EnableLazyLoading="False"
Source="{StaticResource UI_EmotionIcon445}"/>
<TextBlock
Margin="0,5,0,21"
HorizontalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageLaunchGameSelectGamePath}"/>
<Border Style="{ThemeResource BorderCardStyle}"> <Border Style="{ThemeResource BorderCardStyle}">
<ListView <ListView
ItemTemplate="{StaticResource GamePathEntryListTemplate}" ItemTemplate="{StaticResource GamePathEntryListTemplate}"
@@ -375,7 +364,9 @@
HeaderIcon="{shcm:FontIcon Glyph=&#xE7FC;}" HeaderIcon="{shcm:FontIcon Glyph=&#xE7FC;}"
IsClickEnabled="True"> IsClickEnabled="True">
<cwc:SettingsCard.Description> <cwc:SettingsCard.Description>
<TextBlock Foreground="{ThemeResource SystemErrorTextColor}" Text="{shcm:ResourceString Name=ViewPageSettingSetGamePathHint}"/> <StackPanel>
<TextBlock Foreground="{ThemeResource SystemErrorTextColor}" Text="{shcm:ResourceString Name=ViewPageSettingSetGamePathHint}"/>
</StackPanel>
</cwc:SettingsCard.Description> </cwc:SettingsCard.Description>
</cwc:SettingsCard> </cwc:SettingsCard>
</StackPanel> </StackPanel>

View File

@@ -23,10 +23,7 @@
<ScrollViewer shch:ScrollViewerHelper.LeftPanelMaxWidth="800" Style="{StaticResource TwoPanelScrollViewerStyle}"> <ScrollViewer shch:ScrollViewerHelper.LeftPanelMaxWidth="800" Style="{StaticResource TwoPanelScrollViewerStyle}">
<shch:ScrollViewerHelper.RightPanel> <shch:ScrollViewerHelper.RightPanel>
<StackPanel <StackPanel Width="360" Margin="0,16,16,16">
Width="360"
Margin="0,16,16,16"
Spacing="{StaticResource SettingsCardSpacing}">
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}"> <Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Grid Style="{ThemeResource GridCardStyle}"> <Grid Style="{ThemeResource GridCardStyle}">
<Border <Border
@@ -86,27 +83,6 @@
</Grid> </Grid>
</Grid> </Grid>
</Border> </Border>
<cwc:SettingsExpander
Header="{shcm:ResourceString Name=ViewPageSettingElevatedModeHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE7EF;}"
IsExpanded="True">
<cwc:SettingsExpander.Items>
<cwc:SettingsCard
Command="{Binding RestartAsElevatedCommand}"
Description="{shcm:ResourceString Name=ViewPageSettingElevatedModeDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingElevatedModeRestartAction}"
IsClickEnabled="True"
IsEnabled="{Binding HutaoOptions.IsElevated, Converter={StaticResource BoolNegationConverter}}"/>
<cwc:SettingsCard
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutAction}"
Command="{Binding CreateDesktopShortcutCommand}"
Description="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutHeader}"
IsClickEnabled="True"/>
</cwc:SettingsExpander.Items>
</cwc:SettingsExpander>
</StackPanel> </StackPanel>
</shch:ScrollViewerHelper.RightPanel> </shch:ScrollViewerHelper.RightPanel>
<Grid Padding="16" HorizontalAlignment="Left"> <Grid Padding="16" HorizontalAlignment="Left">
@@ -160,17 +136,17 @@
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}" Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperDescription}" Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperHeader}" Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperHeader}"
Visibility="{Binding UserOptions.IsLicensedDeveloper, Converter={StaticResource BoolToVisibilityConverter}}"> Visibility="{Binding UserOptions.IsLicensedDeveloper, Converter={StaticResource BoolToVisibilityConverter}}"/>
<cwc:SettingsCard
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportMaintainerDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportMaintainerHeader}"
Visibility="{Binding UserOptions.IsMaintainer, Converter={StaticResource BoolToVisibilityConverter}}">
<Button <Button
Command="{Binding OpenTestPageCommand}" Command="{Binding OpenTestPageCommand}"
Content="TEST" Content="TEST"
Style="{ThemeResource SettingButtonStyle}"/> Style="{ThemeResource SettingButtonStyle}"/>
</cwc:SettingsCard> </cwc:SettingsCard>
<cwc:SettingsCard
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportMaintainerDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportMaintainerHeader}"
Visibility="{Binding UserOptions.IsMaintainer, Converter={StaticResource BoolToVisibilityConverter}}"/>
<cwc:SettingsCard Description="{Binding UserOptions.GachaLogExpireAtSlim}" Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportGachaLogExpiredAtHeader}"/> <cwc:SettingsCard Description="{Binding UserOptions.GachaLogExpireAtSlim}" Header="{shcm:ResourceString Name=ViewPageSettingHutaoPassportGachaLogExpiredAtHeader}"/>
<cwc:SettingsCard <cwc:SettingsCard
Command="{Binding Passport.OpenRedeemWebsiteCommand}" Command="{Binding Passport.OpenRedeemWebsiteCommand}"
@@ -205,6 +181,15 @@
HeaderIcon="{shcm:FontIcon Glyph=&#xE776;}" HeaderIcon="{shcm:FontIcon Glyph=&#xE776;}"
IsClickEnabled="True"/> IsClickEnabled="True"/>
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingShellExperienceHeader}"/>
<cwc:SettingsCard
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutAction}"
Command="{Binding CreateDesktopShortcutCommand}"
Description="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE7EF;}"
IsClickEnabled="True"/>
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingApperanceHeader}"/> <TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingApperanceHeader}"/>
<cwc:SettingsCard <cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageSettingApperanceLanguageDescription}" Description="{shcm:ResourceString Name=ViewPageSettingApperanceLanguageDescription}"
@@ -234,47 +219,35 @@
Description="{shcm:ResourceString Name=ViewPageSettingKeyShortcutAutoClickingDescription}" Description="{shcm:ResourceString Name=ViewPageSettingKeyShortcutAutoClickingDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingKeyShortcutAutoClickingHeader}" Header="{shcm:ResourceString Name=ViewPageSettingKeyShortcutAutoClickingHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE92E;}"> HeaderIcon="{shcm:FontIcon Glyph=&#xE92E;}">
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal">
<Button <cwc:UniformGrid
MinWidth="32" Margin="16,-12"
MinHeight="32" ColumnSpacing="16"
Padding="0" Columns="2"
VerticalAlignment="Center" Orientation="Horizontal"
Content="&#xEDA7;" RowSpacing="0">
FontFamily="{ThemeResource SymbolThemeFontFamily}" <CheckBox
Style="{ThemeResource SettingButtonStyle}"> MinWidth="64"
<Button.Flyout> VerticalAlignment="Center"
<Flyout FlyoutPresenterStyle="{ThemeResource FlyoutPresenterPadding16And10Style}"> Content="Win"
<cwc:UniformGrid IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasWindows, Mode=TwoWay}"/>
ColumnSpacing="16" <CheckBox
Columns="2" MinWidth="64"
Orientation="Horizontal" VerticalAlignment="Center"
RowSpacing="0"> Content="Ctrl"
<CheckBox IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasControl, Mode=TwoWay}"/>
MinWidth="64" <CheckBox
VerticalAlignment="Center" MinWidth="64"
Content="Win" VerticalAlignment="Center"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasWindows, Mode=TwoWay}"/> Content="Shift"
<CheckBox IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasShift, Mode=TwoWay}"/>
MinWidth="64" <CheckBox
VerticalAlignment="Center" MinWidth="64"
Content="Ctrl" VerticalAlignment="Center"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasControl, Mode=TwoWay}"/> Content="Alt"
<CheckBox IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasAlt, Mode=TwoWay}"/>
MinWidth="64" </cwc:UniformGrid>
VerticalAlignment="Center" <shc:SizeRestrictedContentControl>
Content="Shift"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasShift, Mode=TwoWay}"/>
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Alt"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasAlt, Mode=TwoWay}"/>
</cwc:UniformGrid>
</Flyout>
</Button.Flyout>
</Button>
<shc:SizeRestrictedContentControl VerticalAlignment="Center">
<ComboBox <ComboBox
MinWidth="120" MinWidth="120"
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -335,6 +308,20 @@
</cwc:SettingsCard> </cwc:SettingsCard>
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingGameHeader}"/> <TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{shcm:ResourceString Name=ViewPageSettingGameHeader}"/>
<cwc:SettingsCard
ActionIcon="{shcm:FontIcon Glyph=&#xE76C;}"
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingSetGamePathAction}"
Command="{Binding SetPowerShellPathCommand}"
Header="{shcm:ResourceString Name=ViewPageSettingSetPowerShellPathHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE756;}"
IsClickEnabled="True">
<cwc:SettingsCard.Description>
<StackPanel>
<TextBlock Text="{shcm:ResourceString Name=ViewPageSettingSetPowerShellDescription}"/>
<TextBlock Text="{Binding AppOptions.PowerShellPath}"/>
</StackPanel>
</cwc:SettingsCard.Description>
</cwc:SettingsCard>
<cwc:SettingsCard <cwc:SettingsCard
ActionIcon="{shcm:FontIcon Glyph=&#xE76C;}" ActionIcon="{shcm:FontIcon Glyph=&#xE76C;}"
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingDeleteCacheAction}" ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingDeleteCacheAction}"

View File

@@ -78,17 +78,15 @@
</cwc:SettingsCard> </cwc:SettingsCard>
<cwc:SettingsCard Header="Reset Guide State"> <cwc:SettingsCard Header="Reset Guide State">
<Button <StackPanel Orientation="Horizontal">
Command="{Binding ResetGuideStateCommand}" <Button Command="{Binding ResetGuideStateCommand}" Content="Reset (No restart)"/>
Content="Reset (No restart)" </StackPanel>
Style="{ThemeResource SettingButtonStyle}"/>
</cwc:SettingsCard> </cwc:SettingsCard>
<cwc:SettingsCard Header="Resize MainWindow"> <cwc:SettingsCard Header="Resize MainWindow">
<Button <StackPanel Orientation="Horizontal">
Command="{Binding ResetMainWindowSizeCommand}" <Button Command="{Binding ResetMainWindowSizeCommand}" Content="Reset"/>
Content="Reset" </StackPanel>
Style="{ThemeResource SettingButtonStyle}"/>
</cwc:SettingsCard> </cwc:SettingsCard>
<cwc:SettingsCard Header="Suppress Metadata Initialization"> <cwc:SettingsCard Header="Suppress Metadata Initialization">
@@ -99,10 +97,6 @@
<ToggleSwitch IsOn="{Binding OverrideElevationRequirement, Mode=TwoWay}"/> <ToggleSwitch IsOn="{Binding OverrideElevationRequirement, Mode=TwoWay}"/>
</cwc:SettingsCard> </cwc:SettingsCard>
<cwc:SettingsCard Header="Override Update Version Comparison">
<ToggleSwitch IsOn="{Binding OverrideUpdateVersionComparison, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard <cwc:SettingsCard
Command="{Binding CompensationGachaLogServiceTimeCommand}" Command="{Binding CompensationGachaLogServiceTimeCommand}"
Header="Compensation GachaLog Service Time For 15 Days" Header="Compensation GachaLog Service Time For 15 Days"

View File

@@ -4,19 +4,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shcm="using:Snap.Hutao.Control.Markup" xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shv="using:Snap.Hutao.ViewModel"
Height="44" Height="44"
VerticalAlignment="Top" VerticalAlignment="Top"
d:DataContext="{d:DesignInstance shv:TitleViewModel}"
mc:Ignorable="d"> mc:Ignorable="d">
<mxi:Interaction.Behaviors>
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
</mxi:Interaction.Behaviors>
<Grid x:Name="DragableGrid"> <Grid x:Name="DragableGrid">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition/> <ColumnDefinition/>
@@ -28,7 +19,7 @@
Margin="4,0,0,0" Margin="4,0,0,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Title}" Text="{x:Bind Title}"
TextWrapping="NoWrap"/> TextWrapping="NoWrap"/>
<StackPanel <StackPanel
@@ -39,12 +30,12 @@
<StackPanel <StackPanel
Orientation="Horizontal" Orientation="Horizontal"
Spacing="6" Spacing="6"
Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityConverter}}"> Visibility="{x:Bind RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityConverter}}">
<ToggleButton <ToggleButton
VerticalAlignment="Center" VerticalAlignment="Center"
IsChecked="{Binding HotKeyOptions.IsMouseClickRepeatForeverOn, Mode=OneWay}" IsChecked="{x:Bind HotKeyOptions.IsMouseClickRepeatForeverOn, Mode=OneWay}"
IsHitTestVisible="False" IsHitTestVisible="False"
Visibility="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.IsEnabled, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}"> Visibility="{x:Bind HotKeyOptions.MouseClickRepeatForeverKeyCombination.IsEnabled, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<Grid ColumnSpacing="8"> <Grid ColumnSpacing="8">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition/> <ColumnDefinition/>
@@ -58,7 +49,7 @@
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.DisplayName, Mode=OneWay}"/> Text="{x:Bind HotKeyOptions.MouseClickRepeatForeverKeyCombination.DisplayName, Mode=OneWay}"/>
</Grid> </Grid>
</ToggleButton> </ToggleButton>
</StackPanel> </StackPanel>
@@ -68,7 +59,7 @@
Padding="12,0" Padding="12,0"
ColumnSpacing="12" ColumnSpacing="12"
Style="{ThemeResource GridCardStyle}" Style="{ThemeResource GridCardStyle}"
Visibility="{Binding UpdateStatus, Converter={StaticResource EmptyObjectToVisibilityConverter}, Mode=OneWay}"> Visibility="{x:Bind UpdateStatus, Converter={StaticResource EmptyObjectToVisibilityConverter}, Mode=OneWay}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition/> <ColumnDefinition/>
<ColumnDefinition/> <ColumnDefinition/>
@@ -80,17 +71,17 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Background="Transparent" Background="Transparent"
CornerRadius="{ThemeResource ControlCornerRadius}" CornerRadius="{ThemeResource ControlCornerRadius}"
Maximum="{Binding UpdateStatus.TotalBytes, Mode=OneWay}" Maximum="{x:Bind UpdateStatus.TotalBytes, Mode=OneWay}"
Opacity="{ThemeResource LargeBackgroundProgressBarOpacity}" Opacity="{ThemeResource LargeBackgroundProgressBarOpacity}"
Value="{Binding UpdateStatus.BytesRead, Mode=OneWay}"/> Value="{x:Bind UpdateStatus.BytesRead, Mode=OneWay}"/>
<TextBlock <TextBlock
Grid.Column="0" Grid.Column="0"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{Binding UpdateStatus.ProgressDescription, Mode=OneWay}"/> Text="{x:Bind UpdateStatus.ProgressDescription, Mode=OneWay}"/>
<TextBlock <TextBlock
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{Binding UpdateStatus.VersionDescription, Mode=OneWay}"/> Text="{x:Bind UpdateStatus.VersionDescription, Mode=OneWay}"/>
</Grid> </Grid>
</StackPanel> </StackPanel>
</Grid> </Grid>

View File

@@ -1,9 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.ViewModel; using Snap.Hutao.Core;
using Snap.Hutao.Core.Windowing.HotKey;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Update;
namespace Snap.Hutao.View; namespace Snap.Hutao.View;
@@ -11,16 +17,78 @@ namespace Snap.Hutao.View;
/// 标题视图 /// 标题视图
/// </summary> /// </summary>
[HighQuality] [HighQuality]
[INotifyPropertyChanged]
internal sealed partial class TitleView : UserControl internal sealed partial class TitleView : UserControl
{ {
private CancellationTokenSource checkUpdateTaskCancellationTokenSource = new();
private UpdateStatus? updateStatus;
public TitleView() public TitleView()
{ {
DataContext = Ioc.Default.GetRequiredService<TitleViewModel>(); Loaded += OnTitleViewLoaded;
Unloaded += OnTitleViewUnloaded;
InitializeComponent(); InitializeComponent();
} }
public string Title
{
[SuppressMessage("", "IDE0027")]
get
{
#if DEBUG
return SH.FormatAppDevNameAndVersion(RuntimeOptions.Version);
#else
return SH.FormatAppNameAndVersion(RuntimeOptions.Version);
#endif
}
}
public FrameworkElement DragArea public FrameworkElement DragArea
{ {
get => DragableGrid; get => DragableGrid;
} }
public RuntimeOptions RuntimeOptions { get; } = Ioc.Default.GetRequiredService<RuntimeOptions>();
public HotKeyOptions HotKeyOptions { get; } = Ioc.Default.GetRequiredService<HotKeyOptions>();
public UpdateStatus? UpdateStatus { get => updateStatus; set => SetProperty(ref updateStatus, value); }
private void OnTitleViewLoaded(object sender, RoutedEventArgs e)
{
DoCheckUpdateAsync(checkUpdateTaskCancellationTokenSource.Token).SafeForget();
Loaded -= OnTitleViewLoaded;
}
private void OnTitleViewUnloaded(object sender, RoutedEventArgs e)
{
checkUpdateTaskCancellationTokenSource.Cancel();
Unloaded -= OnTitleViewUnloaded;
}
private async ValueTask DoCheckUpdateAsync(CancellationToken token)
{
IServiceProvider serviceProvider = Ioc.Default;
IUpdateService updateService = serviceProvider.GetRequiredService<IUpdateService>();
IProgressFactory progressFactory = serviceProvider.GetRequiredService<IProgressFactory>();
IProgress<UpdateStatus> progress = progressFactory.CreateForMainThread<UpdateStatus>(status => UpdateStatus = status);
if (await updateService.CheckForUpdateAndDownloadAsync(progress, token).ConfigureAwait(false))
{
ContentDialogResult result = await serviceProvider
.GetRequiredService<IContentDialogFactory>()
.CreateForConfirmCancelAsync(
SH.FormatViewTitileUpdatePackageReadyTitle(UpdateStatus?.Version),
SH.ViewTitileUpdatePackageReadyContent,
ContentDialogButton.Primary)
.ConfigureAwait(false);
if (result == ContentDialogResult.Primary)
{
updateService.LaunchInstaller();
}
}
await serviceProvider.GetRequiredService<ITaskContext>().SwitchToMainThreadAsync();
UpdateStatus = null;
}
} }

View File

@@ -1,27 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
namespace Snap.Hutao.ViewModel.Game;
internal sealed class GameAccountFilter
{
private readonly SchemeType? type;
public GameAccountFilter(SchemeType? type)
{
this.type = type;
}
public bool Filter(object? item)
{
if (type is null)
{
return true;
}
return item is GameAccount account && account.Type == type;
}
}

View File

@@ -1,39 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Notification;
namespace Snap.Hutao.ViewModel.Game;
internal static class LaunchGameShared
{
public static LaunchScheme? GetCurrentLaunchSchemeFromConfigFile(IGameServiceFacade gameService, IInfoBarService infoBarService)
{
ChannelOptions options = gameService.GetChannelOptions();
if (string.IsNullOrEmpty(options.ConfigFilePath))
{
try
{
return KnownLaunchSchemes.Get().Single(scheme => scheme.Equals(options));
}
catch (InvalidOperationException)
{
if (!IgnoredInvalidChannelOptions.Contains(options))
{
// 后台收集
throw ThrowHelper.NotSupported($"不支持的 MultiChannel: {options}");
}
}
}
else
{
infoBarService.Warning(SH.FormatViewModelLaunchGameMultiChannelReadFail(options.ConfigFilePath));
}
return default;
}
}

View File

@@ -1,17 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.WinUI.Collections; using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Control.Extension; using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core; using Snap.Hutao.Core;
using Snap.Hutao.Core.Diagnostics.CodeAnalysis;
using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Progress; using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service; using Snap.Hutao.Service;
using Snap.Hutao.Service.Game; using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Locator; using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Game.Package; using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.PathAbstraction; using Snap.Hutao.Service.Game.PathAbstraction;
@@ -23,7 +23,6 @@ using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using Windows.Win32.Foundation;
namespace Snap.Hutao.ViewModel.Game; namespace Snap.Hutao.ViewModel.Game;
@@ -43,7 +42,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
private readonly IContentDialogFactory contentDialogFactory; private readonly IContentDialogFactory contentDialogFactory;
private readonly LaunchStatusOptions launchStatusOptions; private readonly LaunchStatusOptions launchStatusOptions;
private readonly IGameLocatorFactory gameLocatorFactory; private readonly IGameLocatorFactory gameLocatorFactory;
private readonly ILogger<LaunchGameViewModel> logger;
private readonly IProgressFactory progressFactory; private readonly IProgressFactory progressFactory;
private readonly IInfoBarService infoBarService; private readonly IInfoBarService infoBarService;
private readonly ResourceClient resourceClient; private readonly ResourceClient resourceClient;
@@ -56,13 +54,46 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
private readonly AppOptions appOptions; private readonly AppOptions appOptions;
private LaunchScheme? selectedScheme; private LaunchScheme? selectedScheme;
private AdvancedCollectionView? gameAccountsView; private ObservableCollection<GameAccount>? gameAccounts;
private GameAccount? selectedGameAccount; private GameAccount? selectedGameAccount;
private GameResource? gameResource; private GameResource? gameResource;
private bool gamePathSelectedAndValid; private bool gamePathSelectedAndValid;
private ImmutableList<GamePathEntry> gamePathEntries; private ImmutableList<GamePathEntry> gamePathEntries;
private GamePathEntry? selectedGamePathEntry; private GamePathEntry? selectedGamePathEntry;
private GameAccountFilter? gameAccountFilter;
public List<LaunchScheme> KnownSchemes { get; } = KnownLaunchSchemes.Get();
public LaunchScheme? SelectedScheme
{
get => selectedScheme;
set
{
SetProperty(ref selectedScheme, value, UpdateGameResourceAsync);
async ValueTask UpdateGameResourceAsync(LaunchScheme? scheme)
{
if (scheme is null)
{
return;
}
await taskContext.SwitchToBackgroundAsync();
Web.Response.Response<GameResource> response = await resourceClient
.GetResourceAsync(scheme)
.ConfigureAwait(false);
if (response.IsOk())
{
await taskContext.SwitchToMainThreadAsync();
GameResource = response.Data;
}
}
}
}
public ObservableCollection<GameAccount>? GameAccounts { get => gameAccounts; set => SetProperty(ref gameAccounts, value); }
public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); }
public LaunchOptions LaunchOptions { get => launchOptions; } public LaunchOptions LaunchOptions { get => launchOptions; }
@@ -72,22 +103,8 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
public AppOptions AppOptions { get => appOptions; } public AppOptions AppOptions { get => appOptions; }
public List<LaunchScheme> KnownSchemes { get; } = KnownLaunchSchemes.Get();
[AlsoAsyncSets(nameof(GameResource), nameof(GameAccountsView))]
public LaunchScheme? SelectedScheme
{
get => selectedScheme;
set => SetSelectedSchemeAsync(value).SafeForget();
}
public AdvancedCollectionView? GameAccountsView { get => gameAccountsView; set => SetProperty(ref gameAccountsView, value); }
public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); }
public GameResource? GameResource { get => gameResource; set => SetProperty(ref gameResource, value); } public GameResource? GameResource { get => gameResource; set => SetProperty(ref gameResource, value); }
[AlsoAsyncSets(nameof(SelectedScheme), nameof(GameAccountsView))]
public bool GamePathSelectedAndValid public bool GamePathSelectedAndValid
{ {
get => gamePathSelectedAndValid; get => gamePathSelectedAndValid;
@@ -95,100 +112,112 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
{ {
if (SetProperty(ref gamePathSelectedAndValid, value) && value) if (SetProperty(ref gamePathSelectedAndValid, value) && value)
{ {
RefreshUIAsync().SafeForget(); InitializeUICoreAsync().SafeForget();
}
async ValueTask RefreshUIAsync()
{
try
{
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
{
LaunchScheme? scheme = LaunchGameShared.GetCurrentLaunchSchemeFromConfigFile(gameService, infoBarService);
await taskContext.SwitchToMainThreadAsync();
await SetSelectedSchemeAsync(scheme).ConfigureAwait(true);
TrySetGameAccountByDesiredUid();
// Try set to the current account.
if (SelectedScheme is not null)
{
// The GameAccount is gaurenteed to be in the view, bacause the scheme is synced
SelectedGameAccount ??= gameService.DetectCurrentGameAccount(SelectedScheme);
}
else
{
infoBarService.Warning(SH.ViewModelLaunchGameSchemeNotSelected);
}
}
}
catch (UserdataCorruptedException ex)
{
infoBarService.Error(ex);
}
}
void TrySetGameAccountByDesiredUid()
{
// Sync uid, almost never hit, so we are not so care about performance
if (memoryCache.TryRemove(DesiredUid, out object? value) && value is string uid)
{
ArgumentNullException.ThrowIfNull(GameAccountsView);
// Exists in the source collection
if (GameAccountsView.SourceCollection.Cast<GameAccount>().FirstOrDefault(g => g.AttachUid == uid) is { } sourceAccount)
{
SelectedGameAccount = GameAccountsView.Cast<GameAccount>().FirstOrDefault(g => g.AttachUid == uid);
// But not exists in the view for current scheme
if (SelectedGameAccount is null)
{
infoBarService.Warning(SH.FormatViewModelLaunchGameUnableToSwitchUidAttachedGameAccount(uid, sourceAccount.Name));
}
}
}
} }
} }
} }
public ImmutableList<GamePathEntry> GamePathEntries { get => gamePathEntries; set => SetProperty(ref gamePathEntries, value); } public ImmutableList<GamePathEntry> GamePathEntries { get => gamePathEntries; set => SetProperty(ref gamePathEntries, value); }
[AlsoSets(nameof(GamePathSelectedAndValid))]
public GamePathEntry? SelectedGamePathEntry public GamePathEntry? SelectedGamePathEntry
{ {
get => selectedGamePathEntry; get => selectedGamePathEntry;
set set => UpdateSelectedGamePathEntry(value, true);
{
if (SetProperty(ref selectedGamePathEntry, value, nameof(SelectedGamePathEntry)))
{
if (IsViewDisposed)
{
return;
}
launchOptions.GamePath = value?.Path ?? string.Empty;
GamePathSelectedAndValid = File.Exists(launchOptions.GamePath);
}
}
} }
protected override ValueTask<bool> InitializeUIAsync() protected override ValueTask<bool> InitializeUIAsync()
{ {
SyncGamePathEntriesAndSelectedGamePathEntryFromLaunchOptions(); GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
SelectedGamePathEntry = entry;
return ValueTask.FromResult(true); return ValueTask.FromResult(true);
} }
private async ValueTask InitializeUICoreAsync()
{
try
{
using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
{
ChannelOptions options = gameService.GetChannelOptions();
if (string.IsNullOrEmpty(options.ConfigFilePath))
{
try
{
SelectedScheme = KnownSchemes.Single(scheme => scheme.Equals(options));
}
catch (InvalidOperationException)
{
if (!IgnoredInvalidChannelOptions.Contains(options))
{
// 后台收集
throw new NotSupportedException($"不支持的 MultiChannel: {options}");
}
}
}
else
{
infoBarService.Warning(SH.FormatViewModelLaunchGameMultiChannelReadFail(options.ConfigFilePath));
}
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
await taskContext.SwitchToMainThreadAsync();
GameAccounts = accounts;
// Sync uid
if (memoryCache.TryRemove(DesiredUid, out object? value) && value is string uid)
{
SelectedGameAccount = GameAccounts.FirstOrDefault(g => g.AttachUid == uid);
}
// Try set to the current account.
SelectedGameAccount ??= gameService.DetectCurrentGameAccount();
}
}
catch (UserdataCorruptedException ex)
{
infoBarService.Error(ex);
}
catch (OperationCanceledException)
{
}
}
private void UpdateSelectedGamePathEntry(GamePathEntry? value, bool setBack)
{
if (SetProperty(ref selectedGamePathEntry, value, nameof(SelectedGamePathEntry)) && setBack)
{
if (IsViewDisposed)
{
return;
}
launchOptions.GamePath = value?.Path ?? string.Empty;
GamePathSelectedAndValid = File.Exists(launchOptions.GamePath);
}
}
[Command("SetGamePathCommand")] [Command("SetGamePathCommand")]
private async Task SetGamePathAsync() private async Task SetGamePathAsync()
{ {
(bool isOk, string path) = await gameLocatorFactory.LocateAsync(GameLocationSource.Manual).ConfigureAwait(false); IGameLocator locator = gameLocatorFactory.Create(GameLocationSource.Manual);
(bool isOk, string path) = await locator.LocateGamePathAsync().ConfigureAwait(false);
if (!isOk) if (!isOk)
{ {
return; return;
} }
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
GamePathEntries = launchOptions.UpdateGamePathAndRefreshEntries(path); try
{
GamePathEntries = launchOptions.UpdateGamePathAndRefreshEntries(path);
}
catch (SqliteException ex)
{
// 文件夹权限不足,无法写入数据库
infoBarService.Error(ex, SH.ViewModelSettingSetGamePathDatabaseFailedTitle);
}
} }
[Command("ResetGamePathCommand")] [Command("ResetGamePathCommand")]
@@ -225,20 +254,24 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
// Always ensure game resources // Always ensure game resources
if (!await gameService.EnsureGameResourceAsync(SelectedScheme, convertProgress).ConfigureAwait(false)) if (!await gameService.EnsureGameResourceAsync(SelectedScheme, convertProgress).ConfigureAwait(false))
{ {
infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail, dialog.State.Name); infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail);
return; return;
} }
else else
{ {
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
SyncGamePathEntriesAndSelectedGamePathEntryFromLaunchOptions(); GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
UpdateSelectedGamePathEntry(entry, false);
} }
} }
if (SelectedGameAccount is not null && !gameService.SetGameAccount(SelectedGameAccount)) if (SelectedGameAccount is not null)
{ {
infoBarService.Warning(SH.ViewModelLaunchGameSwitchGameAccountFail); if (!gameService.SetGameAccount(SelectedGameAccount))
return; {
infoBarService.Warning(SH.ViewModelLaunchGameSwitchGameAccountFail);
return;
}
} }
IProgress<LaunchStatus> launchProgress = progressFactory.CreateForMainThread<LaunchStatus>(status => launchStatusOptions.LaunchStatus = status); IProgress<LaunchStatus> launchProgress = progressFactory.CreateForMainThread<LaunchStatus>(status => launchStatusOptions.LaunchStatus = status);
@@ -246,13 +279,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
} }
catch (Exception ex) catch (Exception ex)
{ {
if (ex is Win32Exception win32Exception && win32Exception.HResult == HRESULT.E_FAIL)
{
// User canceled the operation. ignore
return;
}
logger.LogCritical(ex, "Launch failed");
infoBarService.Error(ex); infoBarService.Error(ex);
} }
} }
@@ -262,14 +288,11 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
{ {
try try
{ {
if (SelectedScheme is null) GameAccount? account = await gameService.DetectGameAccountAsync().ConfigureAwait(false);
{
infoBarService.Error(SH.ViewModelLaunchGameSchemeNotSelected);
return;
}
// If user canceled the operation, the return is null // If user canceled the operation, the return is null,
if (await gameService.DetectGameAccountAsync(SelectedScheme).ConfigureAwait(false) is { } account) // and thus we should not set SelectedAccount
if (account is not null)
{ {
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
SelectedGameAccount = account; SelectedGameAccount = account;
@@ -327,54 +350,4 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
await Windows.System.Launcher.LaunchFolderPathAsync(screenshot); await Windows.System.Launcher.LaunchFolderPathAsync(screenshot);
} }
} }
private async ValueTask SetSelectedSchemeAsync(LaunchScheme? value)
{
if (SetProperty(ref selectedScheme, value, nameof(SelectedScheme)))
{
UpdateGameResourceAsync(value).SafeForget();
await UpdateGameAccountsViewAsync().ConfigureAwait(false);
// Clear the selected game account to prevent setting
// incorrect CN/OS account when scheme not match
SelectedGameAccount = default;
}
async ValueTask UpdateGameResourceAsync(LaunchScheme? scheme)
{
if (scheme is null)
{
return;
}
await taskContext.SwitchToBackgroundAsync();
Web.Response.Response<GameResource> response = await resourceClient
.GetResourceAsync(scheme)
.ConfigureAwait(false);
if (response.IsOk())
{
await taskContext.SwitchToMainThreadAsync();
GameResource = response.Data;
}
}
async ValueTask UpdateGameAccountsViewAsync()
{
gameAccountFilter = new(SelectedScheme?.GetSchemeType());
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
await taskContext.SwitchToMainThreadAsync();
GameAccountsView = new(accounts, true)
{
Filter = gameAccountFilter.Filter,
};
}
}
private void SyncGamePathEntriesAndSelectedGamePathEntryFromLaunchOptions()
{
GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
SelectedGamePathEntry = entry;
}
} }

View File

@@ -1,15 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using CommunityToolkit.WinUI.Collections;
using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game; using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Notification; using Snap.Hutao.Service.Notification;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Windows.Win32.Foundation;
namespace Snap.Hutao.ViewModel.Game; namespace Snap.Hutao.ViewModel.Game;
@@ -20,17 +16,17 @@ namespace Snap.Hutao.ViewModel.Game;
[ConstructorGenerated(CallBaseConstructor = true)] [ConstructorGenerated(CallBaseConstructor = true)]
internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSlim<View.Page.LaunchGamePage> internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSlim<View.Page.LaunchGamePage>
{ {
private readonly LaunchStatusOptions launchStatusOptions;
private readonly IProgressFactory progressFactory;
private readonly IInfoBarService infoBarService;
private readonly IGameServiceFacade gameService; private readonly IGameServiceFacade gameService;
private readonly ITaskContext taskContext; private readonly ITaskContext taskContext;
private readonly IInfoBarService infoBarService;
private AdvancedCollectionView? gameAccountsView; private ObservableCollection<GameAccount>? gameAccounts;
private GameAccount? selectedGameAccount; private GameAccount? selectedGameAccount;
private GameAccountFilter? gameAccountFilter;
public AdvancedCollectionView? GameAccountsView { get => gameAccountsView; set => SetProperty(ref gameAccountsView, value); } /// <summary>
/// 游戏账号集合
/// </summary>
public ObservableCollection<GameAccount>? GameAccounts { get => gameAccounts; set => SetProperty(ref gameAccounts, value); }
/// <summary> /// <summary>
/// 选中的账号 /// 选中的账号
@@ -40,29 +36,19 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
/// <inheritdoc/> /// <inheritdoc/>
protected override async Task OpenUIAsync() protected override async Task OpenUIAsync()
{ {
LaunchScheme? scheme = LaunchGameShared.GetCurrentLaunchSchemeFromConfigFile(gameService, infoBarService);
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection; ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
await taskContext.SwitchToMainThreadAsync();
GameAccounts = accounts;
try try
{ {
if (scheme is not null) // Try set to the current account.
{ SelectedGameAccount ??= gameService.DetectCurrentGameAccount();
// Try set to the current account.
SelectedGameAccount ??= gameService.DetectCurrentGameAccount(scheme);
}
} }
catch (UserdataCorruptedException ex) catch (UserdataCorruptedException ex)
{ {
infoBarService.Error(ex); infoBarService.Error(ex);
} }
gameAccountFilter = new(scheme?.GetSchemeType());
await taskContext.SwitchToMainThreadAsync();
GameAccountsView = new(accounts, true)
{
Filter = gameAccountFilter.Filter,
};
} }
[Command("LaunchCommand")] [Command("LaunchCommand")]
@@ -81,17 +67,10 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
} }
} }
IProgress<LaunchStatus> launchProgress = progressFactory.CreateForMainThread<LaunchStatus>(status => launchStatusOptions.LaunchStatus = status); await gameService.LaunchAsync(new Progress<LaunchStatus>()).ConfigureAwait(false);
await gameService.LaunchAsync(launchProgress).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
if (ex is Win32Exception win32Exception && win32Exception.HResult == HRESULT.E_FAIL)
{
// User canceled the operation. ignore
return;
}
infoBarService.Error(ex); infoBarService.Error(ex);
} }
} }

View File

@@ -4,6 +4,7 @@
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.AppLifecycle; using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core; using Snap.Hutao.Core;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.DataTransfer; using Snap.Hutao.Core.IO.DataTransfer;
using Snap.Hutao.Core.Setting; using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Shell; using Snap.Hutao.Core.Shell;
@@ -15,6 +16,7 @@ using Snap.Hutao.Model;
using Snap.Hutao.Service; using Snap.Hutao.Service;
using Snap.Hutao.Service.GachaLog.QueryProvider; using Snap.Hutao.Service.GachaLog.QueryProvider;
using Snap.Hutao.Service.Game; using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Hutao; using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Navigation; using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.Notification; using Snap.Hutao.Service.Notification;
@@ -24,7 +26,6 @@ using Snap.Hutao.ViewModel.Guide;
using Snap.Hutao.Web.Hoyolab; using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hutao; using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Response; using Snap.Hutao.Web.Response;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -46,6 +47,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
private readonly HutaoInfrastructureClient hutaoInfrastructureClient; private readonly HutaoInfrastructureClient hutaoInfrastructureClient;
private readonly HutaoPassportViewModel hutaoPassportViewModel; private readonly HutaoPassportViewModel hutaoPassportViewModel;
private readonly IContentDialogFactory contentDialogFactory; private readonly IContentDialogFactory contentDialogFactory;
private readonly IGameLocatorFactory gameLocatorFactory;
private readonly INavigationService navigationService; private readonly INavigationService navigationService;
private readonly IClipboardProvider clipboardInterop; private readonly IClipboardProvider clipboardInterop;
private readonly IShellLinkInterop shellLinkInterop; private readonly IShellLinkInterop shellLinkInterop;
@@ -172,6 +174,18 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
await Launcher.LaunchUriAsync(new("ms-windows-store://pdp/?productid=9PH4NXJ2JN52")); await Launcher.LaunchUriAsync(new("ms-windows-store://pdp/?productid=9PH4NXJ2JN52"));
} }
[Command("SetPowerShellPathCommand")]
private async Task SetPowerShellPathAsync()
{
(bool isOk, ValueFile file) = fileSystemPickerInteraction.PickFile(SH.FilePickerPowerShellCommit, [("PowerShell", "powershell.exe;pwsh.exe")]);
if (isOk && Path.GetFileNameWithoutExtension(file).EqualsAny(["POWERSHELL", "PWSH"], StringComparison.OrdinalIgnoreCase))
{
await taskContext.SwitchToMainThreadAsync();
AppOptions.PowerShellPath = file;
}
}
[Command("DeleteGameWebCacheCommand")] [Command("DeleteGameWebCacheCommand")]
private void DeleteGameWebCache() private void DeleteGameWebCache()
{ {
@@ -280,17 +294,4 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
infoBarService.Warning(SH.ViewModelSettingCreateDesktopShortcutFailed); infoBarService.Warning(SH.ViewModelSettingCreateDesktopShortcutFailed);
} }
} }
[Command("RestartAsElevatedCommand")]
private void RestartAsElevated()
{
Process.Start(new ProcessStartInfo()
{
FileName = $"shell:AppsFolder\\{runtimeOptions.FamilyName}!App",
UseShellExecute = true,
Verb = "runas",
});
Process.GetCurrentProcess().Kill();
}
} }

View File

@@ -39,13 +39,6 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel
set => LocalSetting.Set(SettingKeys.OverrideElevationRequirement, value); set => LocalSetting.Set(SettingKeys.OverrideElevationRequirement, value);
} }
[SuppressMessage("", "CA1822")]
public bool OverrideUpdateVersionComparison
{
get => LocalSetting.Get(SettingKeys.OverrideUpdateVersionComparison, false);
set => LocalSetting.Set(SettingKeys.OverrideUpdateVersionComparison, value);
}
[Command("ResetGuideStateCommand")] [Command("ResetGuideStateCommand")]
private static void ResetGuideState() private static void ResetGuideState()
{ {

View File

@@ -1,81 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Windowing.HotKey;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Update;
using System.Globalization;
using System.Text;
namespace Snap.Hutao.ViewModel;
[ConstructorGenerated]
[Injection(InjectAs.Singleton)]
internal sealed partial class TitleViewModel : Abstraction.ViewModel
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly IProgressFactory progressFactory;
private readonly RuntimeOptions runtimeOptions;
private readonly HotKeyOptions hotKeyOptions;
private readonly IUpdateService updateService;
private readonly ITaskContext taskContext;
private UpdateStatus? updateStatus;
public RuntimeOptions RuntimeOptions { get => runtimeOptions; }
public HotKeyOptions HotKeyOptions { get => hotKeyOptions; }
public string Title
{
[SuppressMessage("", "IDE0027")]
get
{
string name = new StringBuilder()
.Append("App")
.AppendIf(runtimeOptions.IsElevated, "Elevated")
#if DEBUG
.Append("Dev")
#endif
.Append("NameAndVersion")
.ToString();
string? format = SH.GetString(CultureInfo.CurrentCulture, name);
ArgumentException.ThrowIfNullOrEmpty(format);
return string.Format(CultureInfo.CurrentCulture, format, runtimeOptions.Version);
}
}
public UpdateStatus? UpdateStatus { get => updateStatus; set => SetProperty(ref updateStatus, value); }
protected override async ValueTask<bool> InitializeUIAsync()
{
await DoCheckUpdateAsync().ConfigureAwait(false);
return true;
}
private async ValueTask DoCheckUpdateAsync()
{
IProgress<UpdateStatus> progress = progressFactory.CreateForMainThread<UpdateStatus>(status => UpdateStatus = status);
if (await updateService.CheckForUpdateAndDownloadAsync(progress).ConfigureAwait(false))
{
ContentDialogResult result = await contentDialogFactory
.CreateForConfirmCancelAsync(
SH.FormatViewTitileUpdatePackageReadyTitle(UpdateStatus?.Version),
SH.ViewTitileUpdatePackageReadyContent,
ContentDialogButton.Primary)
.ConfigureAwait(false);
if (result == ContentDialogResult.Primary)
{
await updateService.LaunchUpdaterAsync().ConfigureAwait(false);
}
}
await taskContext.SwitchToMainThreadAsync();
UpdateStatus = null;
}
}

View File

@@ -12,6 +12,7 @@ using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.DataSigning; using Snap.Hutao.Web.Hoyolab.DataSigning;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth; using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
using Snap.Hutao.Web.Response; using Snap.Hutao.Web.Response;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Windows.Foundation; using Windows.Foundation;

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
internal sealed class ArchonQuest
{
[JsonPropertyName("status")]
public ArchonQuestStatus Status { get; set; }
/// <summary>
/// 第X章 第Y幕
/// </summary>
[JsonPropertyName("chapter_num")]
public string ChapterNum { get; set; } = default!;
[JsonPropertyName("chapter_title")]
public string ChapterTitle { get; set; } = default!;
[JsonPropertyName("id")]
public uint Id { get; set; }
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
internal sealed class ArchonQuestProgress
{
[JsonPropertyName("list")]
public List<ArchonQuest> List { get; set; } = default!;
[JsonPropertyName("is_open_archon_quest")]
public bool IsOpenArchonQuest { get; set; }
[JsonPropertyName("is_finish_all_mainline")]
public bool IsFinishAllMainline { get; set; }
[JsonPropertyName("is_finish_all_interchapter")]
public bool IsFinishAllInterchapter { get; set; }
[JsonPropertyName("wiki_url")]
public string WikiUrl { get; set; } = default!;
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
[Localization]
internal enum ArchonQuestStatus
{
[LocalizationKey("WebDailyNoteArchonQuestStatusFinished")]
StatusFinished,
[LocalizationKey("WebDailyNoteArchonQuestStatusOngoing")]
StatusOngoing,
[LocalizationKey("WebDailyNoteArchonQuestStatusNotOpen")]
StatusNotOpen,
}

View File

@@ -7,16 +7,20 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote;
/// 实时便笺 /// 实时便笺
/// </summary> /// </summary>
[HighQuality] [HighQuality]
[SuppressMessage("", "SA1124")]
internal sealed class DailyNote : DailyNoteCommon internal sealed class DailyNote : DailyNoteCommon
{ {
#region Binding /// <summary>
/// 格式化的树脂显示
/// </summary>
[JsonIgnore] [JsonIgnore]
public string ResinFormatted public string ResinFormatted
{ {
get => $"{CurrentResin}/{MaxResin}"; get => $"{CurrentResin}/{MaxResin}";
} }
/// <summary>
/// 格式化的树脂恢复时间
/// </summary>
[JsonIgnore] [JsonIgnore]
public string ResinRecoveryTargetTime public string ResinRecoveryTargetTime
{ {
@@ -41,12 +45,18 @@ internal sealed class DailyNote : DailyNoteCommon
} }
} }
/// <summary>
/// 格式化任务
/// </summary>
[JsonIgnore] [JsonIgnore]
public string TaskFormatted public string TaskFormatted
{ {
get => $"{FinishedTaskNum}/{TotalTaskNum}"; get => $"{FinishedTaskNum}/{TotalTaskNum}";
} }
/// <summary>
/// 每日委托奖励字符串
/// </summary>
[JsonIgnore] [JsonIgnore]
public string ExtraTaskRewardDescription public string ExtraTaskRewardDescription
{ {
@@ -60,24 +70,53 @@ internal sealed class DailyNote : DailyNoteCommon
} }
} }
/// <summary>
/// 剩余周本折扣次数
/// </summary>
[JsonPropertyName("remain_resin_discount_num")]
public int RemainResinDiscountNum { get; set; }
/// <summary>
/// 周本树脂减免使用次数
/// </summary>
[JsonIgnore] [JsonIgnore]
public int ResinDiscountUsedNum public int ResinDiscountUsedNum
{ {
get => ResinDiscountNumLimit - RemainResinDiscountNum; get => ResinDiscountNumLimit - RemainResinDiscountNum;
} }
[JsonIgnore] /// <summary>
/// 周本折扣总次数
/// </summary>
[JsonPropertyName("resin_discount_num_limit")]
public int ResinDiscountNumLimit { get; set; }
/// <summary>
/// 格式化周本
/// </summary>
public string ResinDiscountFormatted public string ResinDiscountFormatted
{ {
get => $"{ResinDiscountUsedNum}/{ResinDiscountNumLimit}"; get => $"{ResinDiscountUsedNum}/{ResinDiscountNumLimit}";
} }
/// <summary>
/// 洞天宝钱恢复时间 <see cref="string"/>类型的秒数
/// </summary>
[JsonPropertyName("home_coin_recovery_time")]
public int HomeCoinRecoveryTime { get; set; }
/// <summary>
/// 格式化洞天宝钱
/// </summary>
[JsonIgnore] [JsonIgnore]
public string HomeCoinFormatted public string HomeCoinFormatted
{ {
get => MaxHomeCoin == 0 ? SH.WebDailyNoteHomeLocked : $"{CurrentHomeCoin}/{MaxHomeCoin}"; get => MaxHomeCoin == 0 ? SH.WebDailyNoteHomeLocked : $"{CurrentHomeCoin}/{MaxHomeCoin}";
} }
/// <summary>
/// 格式化的洞天宝钱恢复时间
/// </summary>
[JsonIgnore] [JsonIgnore]
public string HomeCoinRecoveryTargetTimeFormatted public string HomeCoinRecoveryTargetTimeFormatted
{ {
@@ -95,25 +134,6 @@ internal sealed class DailyNote : DailyNoteCommon
return SH.FormatWebDailyNoteHomeCoinRecoveryFormat(day, reach); return SH.FormatWebDailyNoteHomeCoinRecoveryFormat(day, reach);
} }
} }
#endregion
/// <summary>
/// 剩余周本折扣次数
/// </summary>
[JsonPropertyName("remain_resin_discount_num")]
public int RemainResinDiscountNum { get; set; }
/// <summary>
/// 周本折扣总次数
/// </summary>
[JsonPropertyName("resin_discount_num_limit")]
public int ResinDiscountNumLimit { get; set; }
/// <summary>
/// 洞天宝钱恢复时间 <see cref="string"/>类型的秒数
/// </summary>
[JsonPropertyName("home_coin_recovery_time")]
public int HomeCoinRecoveryTime { get; set; }
/// <summary> /// <summary>
/// 日历链接 /// 日历链接
@@ -129,7 +149,4 @@ internal sealed class DailyNote : DailyNoteCommon
[JsonPropertyName("daily_task")] [JsonPropertyName("daily_task")]
public DailyTask DailyTask { get; set; } = default!; public DailyTask DailyTask { get; set; } = default!;
[JsonPropertyName("archon_quest_progress")]
public ArchonQuestProgress ArchonQuestProgress { get; set; } = default!;
} }