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: 我知道文档站的导航栏中有**搜索功能**,且已经搜索过相关关键词
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
- type: input
@@ -51,7 +51,7 @@ body:
description: |
在胡桃工具箱的设置界面,你可以找到并复制你的设备 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:
required: false
@@ -87,7 +87,7 @@ body:
label: 发生了什么?
description: |
详细的描述问题发生前后的行为,以便我们解决问题。**如果你的问题涉及程序崩溃,你应当检查 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:
required: true

View File

@@ -22,7 +22,7 @@ body:
- label: I and tried **search feature** in Snap Hutao document site, and no associated article
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
- type: input
@@ -51,7 +51,7 @@ body:
description: |
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 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:
required: false
@@ -87,7 +87,7 @@ body:
label: What Happened?
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.
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:
required: true

View File

@@ -12,50 +12,32 @@ body:
value: |
## 创建版本
- [ ] 同步一次 [Crowdin](https://crowdin.com/project/snap-hutao) 翻译
- [ ] 发布 RC 版本Optional
- [ ] 合并入主分支
- [ ] 整理更新内容,等待翻译
- [ ] 打包
- [ ] 提交微软商店
- [ ] 包含更新日志
- [ ] 在 [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),有效期需小于预计发版需要天数
- [ ] 将更新的 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)
- [ ] 在 https://store.rg-adguard.net/ 下载新版本安装包
- [ ] Store URL: https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52
- [ ] 命名格式为 `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
```
- [ ] 通知用户
- [ ] 主分支合并入 release 分支
- [ ] 等待 Release 自动发布
- [ ] 检查极狐是否同步完成 Release
- [ ] 通知用户
- type: checkboxes
id: checklist-final
attributes:
label: Final Check
description: Understand what you are doing
description: Understand what you are doing
options:
- 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
IFileSaveDialog
IPersistFile
IShellLinkDataList
IShellLinkW
ShellLink
SHELL_LINK_DATA_FLAGS
@@ -70,7 +69,6 @@ SHELL_LINK_DATA_FLAGS
IMemoryBufferByteAccess
// Const value
E_FAIL
INFINITE
RPC_E_WRONG_THREAD
MAX_PATH

View File

@@ -18,10 +18,4 @@
TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="6"/>
</Style>
<Style
x:Key="FlyoutPresenterPadding16And10Style"
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="16,10"/>
</Style>
</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_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_EmotionIcon445">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
</ResourceDictionary>

View File

@@ -18,11 +18,11 @@ internal sealed class CommandLineBuilder
/// <summary>
/// 当符合条件时添加参数
/// </summary>
/// <param name="condition">条件</param>
/// <param name="name">参数名称</param>
/// <param name="condition">条件</param>
/// <param name="value">值</param>
/// <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;
}
@@ -35,7 +35,7 @@ internal sealed class CommandLineBuilder
/// <returns>命令行建造器</returns>
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>

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();
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static NotSupportedException NotSupported(string message)
{
throw new NotSupportedException(message);
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
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>
[HighQuality]
[SuppressMessage("", "SA1124")]
internal static class SettingKeys
{
#region MainWindow
public const string WindowRect = "WindowRect";
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 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 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 CultivationAvatarLevelTarget = "CultivationAvatarLevelTarget";
public const string CultivationAvatarSkillACurrent = "CultivationAvatarSkillACurrent";
@@ -43,18 +43,13 @@ internal static class SettingKeys
public const string CultivationWeapon90LevelTarget = "CultivationWeapon90LevelTarget";
public const string CultivationWeapon70LevelCurrent = "CultivationWeapon70LevelCurrent";
public const string CultivationWeapon70LevelTarget = "CultivationWeapon70LevelTarget";
#endregion
#region HomeCard Dashboard
public const string IsHomeCardLaunchGamePresented = "IsHomeCardLaunchGamePresented";
public const string IsHomeCardGachaStatisticsPresented = "IsHomeCardGachaStatisticsPresented";
public const string IsHomeCardAchievementPresented = "IsHomeCardAchievementPresented";
public const string IsHomeCardDailyNotePresented = "IsHomeCardDailyNotePresented";
#endregion
#region DevTool
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
public const string OverrideElevationRequirement = "OverrideElevationRequirement";
public const string OverrideUpdateVersionComparison = "OverrideUpdateVersionComparison";
#endregion
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled";
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service;
using System.IO;
using System.Runtime.InteropServices;
using Windows.Storage;
@@ -18,16 +19,23 @@ namespace Snap.Hutao.Core.Shell;
internal sealed partial class ShellLinkInterop : IShellLinkInterop
{
private readonly RuntimeOptions runtimeOptions;
private readonly AppOptions appOptions;
public async ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync()
{
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
string targetLogoPath = Path.Combine(runtimeOptions.DataFolder, "ShellLinkLogo.ico");
try
{
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
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
{
@@ -37,15 +45,12 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
HRESULT result = CoCreateInstance<ShellLink, IShellLinkW>(null, CLSCTX.CLSCTX_INPROC_SERVER, out IShellLinkW shellLink);
Marshal.ThrowExceptionForHR(result);
shellLink.SetPath($"shell:AppsFolder\\{runtimeOptions.FamilyName}!App");
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL);
shellLink.SetPath(appOptions.PowerShellPath);
shellLink.SetArguments($"""
-Command "Start-Process shell:AppsFolder\{runtimeOptions.FamilyName}!App -verb runas"
""");
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_SHOWMINNOACTIVE);
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 target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk");

View File

@@ -1,24 +1,45 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Dispatching;
namespace Snap.Hutao.Core.Threading;
internal class DispatcherQueueProgress<T> : IProgress<T>
{
private readonly DispatcherQueue dispatcherQueue;
private readonly Action<T> handler;
private readonly SynchronizationContext synchronizationContext;
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;
}
public event EventHandler<T>? ProgressChanged;
public void Report(T value)
{
Action<T> handler = this.handler;
dispatcherQueue.TryEnqueue(() => handler(value));
Action<T>? handler = this.handler;
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>
internal interface ITaskContext
{
SynchronizationContext SynchronizationContext { get; }
void BeginInvokeOnMainThread(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>
[Injection(InjectAs.Singleton, typeof(ITaskContext))]
internal sealed class TaskContext : ITaskContext, ITaskContextUnsafe
internal sealed class TaskContext : ITaskContext
{
private readonly DispatcherQueueSynchronizationContext synchronizationContext;
private readonly DispatcherQueue dispatcherQueue;
@@ -24,7 +24,7 @@ internal sealed class TaskContext : ITaskContext, ITaskContextUnsafe
SynchronizationContext.SetSynchronizationContext(synchronizationContext);
}
public DispatcherQueue DispatcherQueue { get => dispatcherQueue; }
public SynchronizationContext SynchronizationContext { get => synchronizationContext; }
/// <inheritdoc/>
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.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.Factory.Progress;
[ConstructorGenerated]
@@ -13,11 +11,6 @@ internal sealed partial class ProgressFactory : IProgressFactory
public IProgress<T> CreateForMainThread<T>(Action<T> handler)
{
if (taskContext is not ITaskContextUnsafe @unsafe)
{
throw ThrowHelper.NotSupported();
}
return new DispatcherQueueProgress<T>(handler, @unsafe.DispatcherQueue);
return new DispatcherQueueProgress<T>(handler, taskContext.SynchronizationContext);
}
}

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.Model.Entity;
/// </summary>
[HighQuality]
[Table("game_accounts")]
internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount, string, string, SchemeType>
internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount, string, string>
{
/// <summary>
/// 内部Id
@@ -40,17 +40,21 @@ internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount,
/// <summary>
/// [MIHOYOSDK_ADL_PROD_CN_h3123967166]
/// [MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810]
/// </summary>
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()
{
Name = name,
MihoyoSDK = sdk,
Type = type,
};
}

View File

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

View File

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

View File

@@ -93,10 +93,6 @@ internal static class AvatarIds
public static readonly AvatarId Neuvillette = 10000087;
public static readonly AvatarId Charlotte = 10000088;
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>
/// 检查该角色是否为主角

View File

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

View File

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

View File

@@ -60,45 +60,45 @@
: 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: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:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<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="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<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="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<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="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<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:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,6 +186,9 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>Import</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>Select PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>Welcome to Snap Hutao, Traveler ~</value>
</data>
@@ -929,6 +932,9 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>Unable to set registry key without enabling long path</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>PowerShell installation directory not found</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>Unable to read game config file {0}, file may be not exist</value>
</data>
@@ -2426,6 +2432,12 @@
<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>
</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">
<value>Shell Experience</value>
</data>

View File

@@ -60,45 +60,45 @@
: 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: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:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<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="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<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="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<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="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<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:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,6 +186,9 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>Impor</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>Pilih PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>Selamat Datang di Snap Hutao, Traveler ~</value>
</data>
@@ -929,6 +932,9 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>Tidak dapat mengatur kunci registri tanpa mengaktifkan path panjang</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>Direktori instalasi PowerShell tidak ditemukan</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>Tidak dapat membaca file konfigurasi game {0}, file mungkin tidak ada</value>
</data>
@@ -2426,6 +2432,12 @@
<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>
</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">
<value>Pengalaman Shell</value>
</data>

View File

@@ -60,45 +60,45 @@
: 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: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:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<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="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<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="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<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="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<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:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,6 +186,9 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>インポート</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>PowerShellを選択</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>胡桃へようこそ</value>
</data>
@@ -929,6 +932,9 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>長いパスのサポートがオフになっているため、レジストリキーを編集できません。</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>PowerShellのインストールディレクトリが見つかりません</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>ゲーム設定ファイル {0} の読み込みに失敗しました。ファイルが存在していない可能性があります。</value>
</data>
@@ -2426,6 +2432,12 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>ゲームのパスを設定する際、本体YuanShen.exe または GenshinImpact.exeを選んでください。ランチャーlauncher.exeではありません</value>
</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">
<value>Shell エクスペリエンス</value>
</data>

View File

@@ -60,45 +60,45 @@
: 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: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:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<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="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<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="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<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="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<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:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,6 +186,9 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>가져오기</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>选择 PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>欢迎使用胡桃</value>
</data>
@@ -929,6 +932,9 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>긴 경로 기능이 켜지지 않아 레지스트리 키 값을 설정할 수 없습니다</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>PowerShell 설치 경로를 찾을 수 없습니다</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>无法读取游戏配置文件 {0},可能是文件不存在</value>
</data>
@@ -2426,6 +2432,12 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>게임 경로를 설정할 때 런쳐(launcher.exe) 대신 게임(YuanShen.exe 또는 GenshinImpact.exe)를 선택하세요</value>
</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">
<value>Shell 体验</value>
</data>

View File

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

View File

@@ -60,45 +60,45 @@
: 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: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:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<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="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<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="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<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="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<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:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,6 +186,9 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>Импорт</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>Выберите PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>Добро пожаловать в Snap Hutao, путешественник ~</value>
</data>
@@ -929,6 +932,9 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>未开启长路径功能,无法设置注册表键值</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>找不到 PowerShell 的安装目录</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>无法读取游戏配置文件 {0},可能是文件不存在</value>
</data>
@@ -2426,6 +2432,12 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>设置游戏路径时请选择游戏本体YuanShen.exe 或 GenshinImpact.exe而不是启动器launcher.exe</value>
</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">
<value>Shell 体验</value>
</data>

View File

@@ -60,45 +60,45 @@
: 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: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:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<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="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
</xsd:sequence>
<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="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
<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="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<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:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required"/>
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -186,6 +186,9 @@
<data name="FilePickerImportCommit" xml:space="preserve">
<value>匯入</value>
</data>
<data name="FilePickerPowerShellCommit" xml:space="preserve">
<value>選擇 PowerShell</value>
</data>
<data name="GuideWindowTitle" xml:space="preserve">
<value>歡迎使用胡桃</value>
</data>
@@ -929,6 +932,9 @@
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
<value>未開啓長路徑功能,無法設定注冊表鍵值</value>
</data>
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
<value>找不到 PowerShell 的安裝目錄</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>無法讀取遊戲設定檔 {0},可能是檔案不存在</value>
</data>
@@ -2426,6 +2432,12 @@
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
<value>設置游戲路徑時請選擇游戲本體YuanShen.exe 或 GenshinImpact.exe 而不是啓動器launcher.exe</value>
</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">
<value>Shell 體驗</value>
</data>

View File

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

View File

@@ -1,12 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.Primitives;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab;
using System.Globalization;
using System.IO;
namespace Snap.Hutao.Service;
@@ -14,12 +16,42 @@ namespace Snap.Hutao.Service;
[Injection(InjectAs.Singleton)]
internal sealed partial class AppOptions : DbStoreOptions
{
private string? powerShellPath;
private bool? isEmptyHistoryWishVisible;
private BackdropType? backdropType;
private CultureInfo? currentCulture;
private Region? region;
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
{
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);
entryList.ForEach(entry => { entry.UserGameRole = userService.GetUserGameRoleByUid(entry.Uid); });
entries = entryList.ToObservableCollection();
entries = new(entryList);
}
return entries;
@@ -147,7 +147,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
// 发送通知必须早于数据库更新,否则会导致通知重复
await dailyNoteNotificationOperation.SendAsync(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.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using System.Net.Http;
@@ -19,7 +18,7 @@ internal sealed partial class DailyNoteWebhookOperation
private readonly DailyNoteOptions dailyNoteOptions;
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;
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()
.SetRequestUri(targetUri)
.SetHeader("x-uid", $"{playerUid}")
.PostJson(dailyNote);
await builder.TryCatchSendAsync(httpClient, logger, token).ConfigureAwait(false);

View File

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

View File

@@ -4,7 +4,8 @@
using Microsoft.Win32;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
@@ -15,21 +16,52 @@ namespace Snap.Hutao.Service.Game.Account;
/// </summary>
internal static class RegistryInterop
{
private const string ChineseKeyName = @"HKEY_CURRENT_USER\Software\miHoYo\原神";
private const string OverseaKeyName = @"HKEY_CURRENT_USER\Software\miHoYo\Genshin Impact";
private const string SdkChineseValueName = "MIHOYOSDK_ADL_PROD_CN_h3123967166";
private const string SdkOverseaValueName = "MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810";
private const string GenshinPath = @"Software\miHoYo\原神";
private const string GenshinKey = $@"HKEY_CURRENT_USER\{GenshinPath}";
private const string SdkChineseKey = "MIHOYOSDK_ADL_PROD_CN_h3123967166";
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)
{
// 存回注册表的字节需要 '\0' 结尾
byte[] target = [.. Encoding.UTF8.GetBytes(account.MihoyoSDK), 0];
(string keyName, string valueName) = GetKeyValueName(account.Type);
Registry.SetValue(keyName, valueName, target);
Encoding.UTF8.GetByteCount(account.MihoyoSDK);
byte[] tempBytes = Encoding.UTF8.GetBytes(account.MihoyoSDK);
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;
}
@@ -38,31 +70,24 @@ internal static class RegistryInterop
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(keyName, valueName, Array.Empty<byte>());
object? sdk = Registry.GetValue(GenshinKey, SdkChineseKey, 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)
{
// 从注册表获取的字节数组带有 '\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}"),
};
return null;
}
}

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.Game.Account;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package;
@@ -52,15 +51,15 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
}
/// <inheritdoc/>
public ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme)
public ValueTask<GameAccount?> DetectGameAccountAsync()
{
return gameAccountService.DetectGameAccountAsync(scheme);
return gameAccountService.DetectGameAccountAsync();
}
/// <inheritdoc/>
public GameAccount? DetectCurrentGameAccount(SchemeType scheme)
public GameAccount? DetectCurrentGameAccount()
{
return gameAccountService.DetectCurrentGameAccount(scheme);
return gameAccountService.DetectCurrentGameAccount();
}
/// <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.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.Scheme;
@@ -29,7 +28,7 @@ internal interface IGameServiceFacade
/// <param name="uid">uid</param>
void AttachGameAccountToUid(GameAccount gameAccount, string uid);
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme);
ValueTask<GameAccount?> DetectGameAccountAsync();
/// <summary>
/// 异步获取游戏路径
@@ -87,5 +86,9 @@ internal interface IGameServiceFacade
/// <returns>是否更改了ini文件</returns>
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
{
public static bool TryGetGamePathAndGameDirectory(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameDirectory)
{
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)
public static bool TryGetGameFolderAndFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName)
{
string gamePath = options.GamePath;
gameDirectory = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameDirectory))
gameFolder = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameFolder))
{
gameFileName = default;
return false;
@@ -55,18 +42,6 @@ internal static class LaunchOptionsExtension
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)
{
string gamePath = options.GamePath;

View File

@@ -4,13 +4,20 @@
namespace Snap.Hutao.Service.Game.Locator;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IGameLocatorFactory))]
[Injection(InjectAs.Transient, typeof(IGameLocatorFactory))]
internal sealed partial class GameLocatorFactory : IGameLocatorFactory
{
[SuppressMessage("", "SH301")]
private readonly IServiceProvider serviceProvider;
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>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.Manual)]
[Injection(InjectAs.Transient)]
internal sealed partial class ManualGameLocator : IGameLocator
{
private readonly IFileSystemPickerInteraction fileSystemPickerInteraction;
@@ -26,7 +26,7 @@ internal sealed partial class ManualGameLocator : IGameLocator
if (isPickerOk)
{
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));
}

View File

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

View File

@@ -12,7 +12,7 @@ namespace Snap.Hutao.Service.Game.Locator;
/// </summary>
[HighQuality]
[ConstructorGenerated]
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.UnityLog)]
[Injection(InjectAs.Transient)]
internal sealed partial class UnityLogGameLocator : IGameLocator
{
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)
{
if (!launchOptions.TryGetGameDirectoryAndGameFileName(out string? gameFolder, out string? gameFileName))
if (!launchOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName))
{
return false;
}
@@ -47,7 +47,8 @@ internal sealed partial class GamePackageService : IGamePackageService
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))
{
return false;
@@ -66,13 +67,6 @@ internal sealed partial class GamePackageService : IGamePackageService
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
{
string tempFilePath = Path.Combine(folder, $"{Guid.NewGuid():N}.tmp");

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,6 @@ using System.IO.Compression;
using System.Net.Http;
using System.Text.RegularExpressions;
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;
@@ -59,15 +58,15 @@ internal sealed partial class PackageConverter
string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath;
string pkgVersionUrl = $"{scatteredFilesUrl}/{PackageVersion}";
PackageConverterFileSystemContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl);
PackageConvertContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl);
// Step 1
progress.Report(new(SH.ServiceGamePackageRequestPackageVerion));
RelativePathVersionItemDictionary remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false);
RelativePathVersionItemDictionary localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false);
Dictionary<string, VersionItem> remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false);
Dictionary<string, VersionItem> localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false);
// Step 2
List<PackageItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList();
List<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList();
diffOperations.SortBy(i => i.Type);
// 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)
{
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 的项,需要替换为服务器上的项
yield return new(PackageItemOperationType.Replace, remoteItem, localItem);
yield return new(ItemOperationType.Replace, remoteItem, localItem);
}
// 同名同MD5跳过
@@ -135,22 +134,22 @@ internal sealed partial class PackageConverter
else
{
// 本地没有发现同名项
yield return new(PackageItemOperationType.Add, remoteItem, remoteItem);
yield return new(ItemOperationType.Add, remoteItem, remoteItem);
}
}
foreach ((_, VersionItem localItem) in local)
{
yield return new(PackageItemOperationType.Backup, localItem, localItem);
yield return new(ItemOperationType.Backup, localItem, localItem);
}
}
[GeneratedRegex("^(?:YuanShen_Data|GenshinImpact_Data)(?=/)")]
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))
{
while (await reader.ReadLineAsync().ConfigureAwait(false) is { Length: > 0 } row)
@@ -165,7 +164,7 @@ internal sealed partial class PackageConverter
return results;
}
private async ValueTask<RelativePathVersionItemDictionary> GetRemoteItemsAsync(string pkgVersionUrl)
private async ValueTask<Dictionary<string, VersionItem>> GetRemoteItemsAsync(string pkgVersionUrl)
{
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)))
{
@@ -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)
{
case PackageItemOperationType.Backup:
case ItemOperationType.Backup:
continue;
case PackageItemOperationType.Replace:
case PackageItemOperationType.Add:
case ItemOperationType.Replace:
case ItemOperationType.Add:
await SkipOrDownloadAsync(info, context, progress).ConfigureAwait(false);
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);
@@ -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
{
PackageItemOperationType.Backup => (true, false),
PackageItemOperationType.Replace => (true, true),
PackageItemOperationType.Add => (false, true),
ItemOperationType.Backup => (true, false),
ItemOperationType.Replace => (true, true),
ItemOperationType.Add => (false, true),
_ => (false, false),
};
@@ -322,7 +321,7 @@ internal sealed partial class PackageConverter
return true;
}
private async ValueTask ReplacePackageVersionFilesAsync(PackageConverterFileSystemContext context)
private async ValueTask ReplacePackageVersionFilesAsync(PackageConvertContext context)
{
foreach (string versionFilePath in Directory.EnumerateFiles(context.GameFolder, "*pkg_version"))
{

View File

@@ -2,13 +2,14 @@
// Licensed under the MIT license.
using CommunityToolkit.Common;
using Snap.Hutao.Core.Abstraction;
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 包更新状态
/// </summary>
internal sealed class PackageReplaceStatus
internal sealed class PackageReplaceStatus : ICloneable<PackageReplaceStatus>
{
/// <summary>
/// 构造一个新的包更新状态
@@ -33,6 +34,10 @@ internal sealed class PackageReplaceStatus
Description = $"{Converters.ToFileSizeString(bytesRead)}/{Converters.ToFileSizeString(totalBytes)}";
}
private PackageReplaceStatus()
{
}
public string Name { get; set; } = default!;
/// <summary>
@@ -49,4 +54,19 @@ internal sealed class PackageReplaceStatus
/// 是否有进度
/// </summary>
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))]
internal sealed partial class GamePathService : IGamePathService
{
private readonly IGameLocatorFactory gameLocatorFactory;
private readonly IServiceProvider serviceProvider;
private readonly LaunchOptions launchOptions;
public async ValueTask<ValueResult<bool, string>> SilentGetGamePathAsync()
@@ -17,16 +17,24 @@ internal sealed partial class GamePathService : IGamePathService
// Cannot find in setting
if (string.IsNullOrEmpty(launchOptions.GamePath))
{
IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService<IGameLocatorFactory>();
bool isOk;
string path;
// 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)
{
// 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)
@@ -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(true, launchOptions.GamePath);
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Service.Discord;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Game.Unlocker;
@@ -19,7 +18,6 @@ namespace Snap.Hutao.Service.Game.Process;
internal sealed partial class GameProcessService : IGameProcessService
{
private readonly IServiceProvider serviceProvider;
private readonly IProgressFactory progressFactory;
private readonly IDiscordService discordService;
private readonly RuntimeOptions runtimeOptions;
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.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html
commandLine = new CommandLineBuilder()
.AppendIf(launchOptions.IsBorderless, "-popupwindow")
.AppendIf(launchOptions.IsExclusive, "-window-mode", "exclusive")
.AppendIf("-popupwindow", launchOptions.IsBorderless)
.AppendIf("-window-mode", launchOptions.IsExclusive, "exclusive")
.Append("-screen-fullscreen", launchOptions.IsFullScreen ? 1 : 0)
.AppendIf(launchOptions.IsScreenWidthEnabled, "-screen-width", launchOptions.ScreenWidth)
.AppendIf(launchOptions.IsScreenHeightEnabled, "-screen-height", launchOptions.ScreenHeight)
.AppendIf(launchOptions.IsMonitorEnabled, "-monitor", launchOptions.Monitor.Value)
.AppendIf(launchOptions.IsUseCloudThirdPartyMobile, "-platform_type CLOUD_THIRD_PARTY_MOBILE")
.AppendIf("-screen-width", launchOptions.IsScreenWidthEnabled, launchOptions.ScreenWidth)
.AppendIf("-screen-height", launchOptions.IsScreenHeightEnabled, launchOptions.ScreenHeight)
.AppendIf("-monitor", launchOptions.IsMonitorEnabled, launchOptions.Monitor.Value)
.AppendIf("-platform_type CLOUD_THIRD_PARTY_MOBILE", launchOptions.IsUseCloudThirdPartyMobile)
.ToString();
}
@@ -140,7 +138,7 @@ internal sealed partial class GameProcessService : IGameProcessService
IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game);
#pragma warning restore CA1859
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);
}

View File

@@ -59,11 +59,11 @@ internal class LaunchScheme : IEquatable<ChannelOptions>
public static bool ExecutableIsOversea(string gameFileName)
{
return gameFileName.ToUpperInvariant() switch
return gameFileName switch
{
GameConstants.GenshinImpactFileNameUpper => true,
GameConstants.YuanShenFileNameUpper => false,
_ => throw Requires.Fail("Invalid game executable file name{0}", gameFileName),
GameConstants.GenshinImpactFileName => true,
GameConstants.YuanShenFileName => false,
_ => 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;
rip += 5U;
rip += (nuint)(*(int*)(rip + 2U) + 6);
rip += (nuint)(*(int*)(rip + 2) + 6);
nuint address = userAssembly.Address + (rip - localMemoryUserAssemblyAddress);
@@ -236,8 +236,6 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
SpinWait.SpinUntil(() => UnsafeReadProcessMemory(gameProcess, address, out ptr) && ptr != 0);
rip = ptr - unityPlayer.Address + localMemoryUnityPlayerAddress;
// CALL or JMP
while (*(byte*)rip == 0xE8 || *(byte*)rip == 0xE9)
{
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 LaunchUpdaterAsync();
void LaunchInstaller();
}

View File

@@ -4,14 +4,12 @@
using Snap.Hutao.Core;
using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Core.IO.Http.Sharding;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.Response;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using Windows.Storage;
namespace Snap.Hutao.Service.Update;
@@ -19,8 +17,6 @@ namespace Snap.Hutao.Service.Update;
[Injection(InjectAs.Singleton, typeof(IUpdateService))]
internal sealed partial class UpdateService : IUpdateService
{
private const string UpdaterFilename = "Snap.Hutao.Deployment.exe";
private readonly IServiceProvider serviceProvider;
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;
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);
}
return false;
File.Delete(msixPath);
}
return false;
}
progress.Report(new(versionInformation.Version.ToString(), 0, 0));
if (versionInformation.Sha256 is not { Length: > 0 } sha256)
if (versionInformation.Sha256 is not { } sha256)
{
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()
{
Arguments = commandLine,
WindowStyle = ProcessWindowStyle.Minimized,
FileName = updaterTargetPath,
UseShellExecute = true,
FileName = GetUpdatePackagePath(),
});
}
@@ -100,10 +78,12 @@ internal sealed partial class UpdateService : IUpdateService
return string.Equals(localHash, remoteHash, StringComparison.OrdinalIgnoreCase);
}
private string GetUpdatePackagePath(RuntimeOptions? runtimeOptions = default)
private string GetUpdatePackagePath()
{
runtimeOptions ??= serviceProvider.GetRequiredService<RuntimeOptions>();
return runtimeOptions.GetDataFolderUpdateCacheFolderFile("Snap.Hutao.msix");
string dataFolder = serviceProvider.GetRequiredService<RuntimeOptions>().DataFolder;
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)

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao.Service.User;
[ConstructorGenerated]
[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 IUserInitializationService userInitializationService;
@@ -22,7 +22,7 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
private readonly ITaskContext taskContext;
private readonly IMessenger messenger;
private readonly SemaphoreSlim throttler = new(1);
private readonly Throttler throttler = new();
private ObservableCollection<BindingUser>? userCollection;
private Dictionary<string, BindingUser>? midUserMap;
@@ -38,9 +38,7 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
public async ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync()
{
// Force run in background thread, otherwise will cause reentrance
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
using (await throttler.EnterAsync().ConfigureAwait(false))
using (await throttler.ThrottleAsync().ConfigureAwait(false))
{
if (userCollection is null)
{
@@ -133,7 +131,17 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
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)
@@ -178,9 +186,4 @@ internal sealed partial class UserCollectionService : IUserCollectionService, ID
ArgumentNullException.ThrowIfNull(newUser.UserInfo);
return new(UserOptionResult.Added, newUser.UserInfo.Uid);
}
public void Dispose()
{
throttler.Dispose();
}
}

View File

@@ -204,10 +204,8 @@
<AdditionalFiles Include="stylecop.json" />
<AdditionalFiles Include="Resource\Localization\SH.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.ko.resx" />
<AdditionalFiles Include="Resource\Localization\SH.ru.resx" />
<AdditionalFiles Include="Resource\Localization\SH.zh-Hant.resx" />
</ItemGroup>
@@ -303,19 +301,11 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" 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.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.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231115000" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<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">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,6 @@
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shccs="using:Snap.Hutao.Control.Collection.Selector"
xmlns:shch="using:Snap.Hutao.Control.Helper"
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
xmlns:shvc="using:Snap.Hutao.View.Control"
xmlns:shvg="using:Snap.Hutao.ViewModel.Game"
@@ -198,7 +197,7 @@
<Border Style="{StaticResource BorderCardStyle}">
<ListView
ItemTemplate="{StaticResource GameAccountListTemplate}"
ItemsSource="{Binding GameAccountsView}"
ItemsSource="{Binding GameAccounts}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
</Border>
@@ -347,20 +346,10 @@
</Grid>
<Grid Visibility="{Binding GamePathSelectedAndValid, Converter={StaticResource BoolToVisibilityRevertConverter}}">
<StackPanel
Margin="128,0"
MaxWidth="600"
HorizontalAlignment="Center"
VerticalAlignment="Center"
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}">
<ListView
ItemTemplate="{StaticResource GamePathEntryListTemplate}"
@@ -375,7 +364,9 @@
HeaderIcon="{shcm:FontIcon Glyph=&#xE7FC;}"
IsClickEnabled="True">
<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>
</StackPanel>

View File

@@ -23,10 +23,7 @@
<ScrollViewer shch:ScrollViewerHelper.LeftPanelMaxWidth="800" Style="{StaticResource TwoPanelScrollViewerStyle}">
<shch:ScrollViewerHelper.RightPanel>
<StackPanel
Width="360"
Margin="0,16,16,16"
Spacing="{StaticResource SettingsCardSpacing}">
<StackPanel Width="360" Margin="0,16,16,16">
<Border cw:Effects.Shadow="{ThemeResource CompatCardShadow}">
<Grid Style="{ThemeResource GridCardStyle}">
<Border
@@ -86,27 +83,6 @@
</Grid>
</Grid>
</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>
</shch:ScrollViewerHelper.RightPanel>
<Grid Padding="16" HorizontalAlignment="Left">
@@ -160,17 +136,17 @@
Background="{ThemeResource SystemFillColorSuccessBackgroundBrush}"
Description="{shcm:ResourceString Name=ViewPageSettingHutaoPassportLicensedDeveloperDescription}"
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
Command="{Binding OpenTestPageCommand}"
Content="TEST"
Style="{ThemeResource SettingButtonStyle}"/>
</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
Command="{Binding Passport.OpenRedeemWebsiteCommand}"
@@ -205,6 +181,15 @@
HeaderIcon="{shcm:FontIcon Glyph=&#xE776;}"
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}"/>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageSettingApperanceLanguageDescription}"
@@ -234,47 +219,35 @@
Description="{shcm:ResourceString Name=ViewPageSettingKeyShortcutAutoClickingDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingKeyShortcutAutoClickingHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE92E;}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
MinWidth="32"
MinHeight="32"
Padding="0"
VerticalAlignment="Center"
Content="&#xEDA7;"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Style="{ThemeResource SettingButtonStyle}">
<Button.Flyout>
<Flyout FlyoutPresenterStyle="{ThemeResource FlyoutPresenterPadding16And10Style}">
<cwc:UniformGrid
ColumnSpacing="16"
Columns="2"
Orientation="Horizontal"
RowSpacing="0">
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Win"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasWindows, Mode=TwoWay}"/>
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Ctrl"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasControl, Mode=TwoWay}"/>
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
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">
<StackPanel Orientation="Horizontal">
<cwc:UniformGrid
Margin="16,-12"
ColumnSpacing="16"
Columns="2"
Orientation="Horizontal"
RowSpacing="0">
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Win"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasWindows, Mode=TwoWay}"/>
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
Content="Ctrl"
IsChecked="{Binding HotKeyOptions.MouseClickRepeatForeverKeyCombination.ModifierHasControl, Mode=TwoWay}"/>
<CheckBox
MinWidth="64"
VerticalAlignment="Center"
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>
<shc:SizeRestrictedContentControl>
<ComboBox
MinWidth="120"
VerticalAlignment="Center"
@@ -335,6 +308,20 @@
</cwc:SettingsCard>
<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
ActionIcon="{shcm:FontIcon Glyph=&#xE76C;}"
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingDeleteCacheAction}"

View File

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

View File

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

View File

@@ -1,9 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
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;
@@ -11,16 +17,78 @@ namespace Snap.Hutao.View;
/// 标题视图
/// </summary>
[HighQuality]
[INotifyPropertyChanged]
internal sealed partial class TitleView : UserControl
{
private CancellationTokenSource checkUpdateTaskCancellationTokenSource = new();
private UpdateStatus? updateStatus;
public TitleView()
{
DataContext = Ioc.Default.GetRequiredService<TitleViewModel>();
Loaded += OnTitleViewLoaded;
Unloaded += OnTitleViewUnloaded;
InitializeComponent();
}
public string Title
{
[SuppressMessage("", "IDE0027")]
get
{
#if DEBUG
return SH.FormatAppDevNameAndVersion(RuntimeOptions.Version);
#else
return SH.FormatAppNameAndVersion(RuntimeOptions.Version);
#endif
}
}
public FrameworkElement DragArea
{
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.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Collections;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Diagnostics.CodeAnalysis;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Game.Package;
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.ObjectModel;
using System.IO;
using Windows.Win32.Foundation;
namespace Snap.Hutao.ViewModel.Game;
@@ -43,7 +42,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
private readonly IContentDialogFactory contentDialogFactory;
private readonly LaunchStatusOptions launchStatusOptions;
private readonly IGameLocatorFactory gameLocatorFactory;
private readonly ILogger<LaunchGameViewModel> logger;
private readonly IProgressFactory progressFactory;
private readonly IInfoBarService infoBarService;
private readonly ResourceClient resourceClient;
@@ -56,13 +54,46 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
private readonly AppOptions appOptions;
private LaunchScheme? selectedScheme;
private AdvancedCollectionView? gameAccountsView;
private ObservableCollection<GameAccount>? gameAccounts;
private GameAccount? selectedGameAccount;
private GameResource? gameResource;
private bool gamePathSelectedAndValid;
private ImmutableList<GamePathEntry> gamePathEntries;
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; }
@@ -72,22 +103,8 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
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); }
[AlsoAsyncSets(nameof(SelectedScheme), nameof(GameAccountsView))]
public bool GamePathSelectedAndValid
{
get => gamePathSelectedAndValid;
@@ -95,100 +112,112 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
{
if (SetProperty(ref gamePathSelectedAndValid, value) && value)
{
RefreshUIAsync().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));
}
}
}
InitializeUICoreAsync().SafeForget();
}
}
}
public ImmutableList<GamePathEntry> GamePathEntries { get => gamePathEntries; set => SetProperty(ref gamePathEntries, value); }
[AlsoSets(nameof(GamePathSelectedAndValid))]
public GamePathEntry? SelectedGamePathEntry
{
get => selectedGamePathEntry;
set
{
if (SetProperty(ref selectedGamePathEntry, value, nameof(SelectedGamePathEntry)))
{
if (IsViewDisposed)
{
return;
}
launchOptions.GamePath = value?.Path ?? string.Empty;
GamePathSelectedAndValid = File.Exists(launchOptions.GamePath);
}
}
set => UpdateSelectedGamePathEntry(value, true);
}
protected override ValueTask<bool> InitializeUIAsync()
{
SyncGamePathEntriesAndSelectedGamePathEntryFromLaunchOptions();
GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
SelectedGamePathEntry = entry;
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")]
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)
{
return;
}
await taskContext.SwitchToMainThreadAsync();
GamePathEntries = launchOptions.UpdateGamePathAndRefreshEntries(path);
try
{
GamePathEntries = launchOptions.UpdateGamePathAndRefreshEntries(path);
}
catch (SqliteException ex)
{
// 文件夹权限不足,无法写入数据库
infoBarService.Error(ex, SH.ViewModelSettingSetGamePathDatabaseFailedTitle);
}
}
[Command("ResetGamePathCommand")]
@@ -225,20 +254,24 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
// Always ensure game resources
if (!await gameService.EnsureGameResourceAsync(SelectedScheme, convertProgress).ConfigureAwait(false))
{
infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail, dialog.State.Name);
infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail);
return;
}
else
{
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);
return;
if (!gameService.SetGameAccount(SelectedGameAccount))
{
infoBarService.Warning(SH.ViewModelLaunchGameSwitchGameAccountFail);
return;
}
}
IProgress<LaunchStatus> launchProgress = progressFactory.CreateForMainThread<LaunchStatus>(status => launchStatusOptions.LaunchStatus = status);
@@ -246,13 +279,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
}
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);
}
}
@@ -262,14 +288,11 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
{
try
{
if (SelectedScheme is null)
{
infoBarService.Error(SH.ViewModelLaunchGameSchemeNotSelected);
return;
}
GameAccount? account = await gameService.DetectGameAccountAsync().ConfigureAwait(false);
// If user canceled the operation, the return is null
if (await gameService.DetectGameAccountAsync(SelectedScheme).ConfigureAwait(false) is { } account)
// If user canceled the operation, the return is null,
// and thus we should not set SelectedAccount
if (account is not null)
{
await taskContext.SwitchToMainThreadAsync();
SelectedGameAccount = account;
@@ -327,54 +350,4 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
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.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Collections;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Service.Notification;
using System.Collections.ObjectModel;
using Windows.Win32.Foundation;
namespace Snap.Hutao.ViewModel.Game;
@@ -20,17 +16,17 @@ namespace Snap.Hutao.ViewModel.Game;
[ConstructorGenerated(CallBaseConstructor = true)]
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 ITaskContext taskContext;
private readonly IInfoBarService infoBarService;
private AdvancedCollectionView? gameAccountsView;
private ObservableCollection<GameAccount>? gameAccounts;
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>
/// 选中的账号
@@ -40,29 +36,19 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
/// <inheritdoc/>
protected override async Task OpenUIAsync()
{
LaunchScheme? scheme = LaunchGameShared.GetCurrentLaunchSchemeFromConfigFile(gameService, infoBarService);
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
await taskContext.SwitchToMainThreadAsync();
GameAccounts = accounts;
try
{
if (scheme is not null)
{
// Try set to the current account.
SelectedGameAccount ??= gameService.DetectCurrentGameAccount(scheme);
}
// Try set to the current account.
SelectedGameAccount ??= gameService.DetectCurrentGameAccount();
}
catch (UserdataCorruptedException ex)
{
infoBarService.Error(ex);
}
gameAccountFilter = new(scheme?.GetSchemeType());
await taskContext.SwitchToMainThreadAsync();
GameAccountsView = new(accounts, true)
{
Filter = gameAccountFilter.Filter,
};
}
[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(launchProgress).ConfigureAwait(false);
await gameService.LaunchAsync(new Progress<LaunchStatus>()).ConfigureAwait(false);
}
catch (Exception ex)
{
if (ex is Win32Exception win32Exception && win32Exception.HResult == HRESULT.E_FAIL)
{
// User canceled the operation. ignore
return;
}
infoBarService.Error(ex);
}
}

View File

@@ -4,6 +4,7 @@
using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.DataTransfer;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Shell;
@@ -15,6 +16,7 @@ using Snap.Hutao.Model;
using Snap.Hutao.Service;
using Snap.Hutao.Service.GachaLog.QueryProvider;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.Notification;
@@ -24,7 +26,6 @@ using Snap.Hutao.ViewModel.Guide;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Response;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
@@ -46,6 +47,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
private readonly HutaoInfrastructureClient hutaoInfrastructureClient;
private readonly HutaoPassportViewModel hutaoPassportViewModel;
private readonly IContentDialogFactory contentDialogFactory;
private readonly IGameLocatorFactory gameLocatorFactory;
private readonly INavigationService navigationService;
private readonly IClipboardProvider clipboardInterop;
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"));
}
[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")]
private void DeleteGameWebCache()
{
@@ -280,17 +294,4 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
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);
}
[SuppressMessage("", "CA1822")]
public bool OverrideUpdateVersionComparison
{
get => LocalSetting.Get(SettingKeys.OverrideUpdateVersionComparison, false);
set => LocalSetting.Set(SettingKeys.OverrideUpdateVersionComparison, value);
}
[Command("ResetGuideStateCommand")]
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.Takumi.Auth;
using Snap.Hutao.Web.Response;
using System.Runtime.InteropServices;
using System.Text;
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>
[HighQuality]
[SuppressMessage("", "SA1124")]
internal sealed class DailyNote : DailyNoteCommon
{
#region Binding
/// <summary>
/// 格式化的树脂显示
/// </summary>
[JsonIgnore]
public string ResinFormatted
{
get => $"{CurrentResin}/{MaxResin}";
}
/// <summary>
/// 格式化的树脂恢复时间
/// </summary>
[JsonIgnore]
public string ResinRecoveryTargetTime
{
@@ -41,12 +45,18 @@ internal sealed class DailyNote : DailyNoteCommon
}
}
/// <summary>
/// 格式化任务
/// </summary>
[JsonIgnore]
public string TaskFormatted
{
get => $"{FinishedTaskNum}/{TotalTaskNum}";
}
/// <summary>
/// 每日委托奖励字符串
/// </summary>
[JsonIgnore]
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]
public int ResinDiscountUsedNum
{
get => ResinDiscountNumLimit - RemainResinDiscountNum;
}
[JsonIgnore]
/// <summary>
/// 周本折扣总次数
/// </summary>
[JsonPropertyName("resin_discount_num_limit")]
public int ResinDiscountNumLimit { get; set; }
/// <summary>
/// 格式化周本
/// </summary>
public string ResinDiscountFormatted
{
get => $"{ResinDiscountUsedNum}/{ResinDiscountNumLimit}";
}
/// <summary>
/// 洞天宝钱恢复时间 <see cref="string"/>类型的秒数
/// </summary>
[JsonPropertyName("home_coin_recovery_time")]
public int HomeCoinRecoveryTime { get; set; }
/// <summary>
/// 格式化洞天宝钱
/// </summary>
[JsonIgnore]
public string HomeCoinFormatted
{
get => MaxHomeCoin == 0 ? SH.WebDailyNoteHomeLocked : $"{CurrentHomeCoin}/{MaxHomeCoin}";
}
/// <summary>
/// 格式化的洞天宝钱恢复时间
/// </summary>
[JsonIgnore]
public string HomeCoinRecoveryTargetTimeFormatted
{
@@ -95,25 +134,6 @@ internal sealed class DailyNote : DailyNoteCommon
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>
/// 日历链接
@@ -129,7 +149,4 @@ internal sealed class DailyNote : DailyNoteCommon
[JsonPropertyName("daily_task")]
public DailyTask DailyTask { get; set; } = default!;
[JsonPropertyName("archon_quest_progress")]
public ArchonQuestProgress ArchonQuestProgress { get; set; } = default!;
}