mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
1 Commits
feat/dynam
...
Masterain9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b36399f572 |
@@ -61,6 +61,9 @@ release:
|
|||||||
- name: "$THIS_SHA256SUMS_NAME"
|
- 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"
|
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/raw/$THIS_SHA256SUMS_NAME?inline=false"
|
||||||
link_type: other
|
link_type: other
|
||||||
|
- name: "artifact_archive"
|
||||||
|
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/download?file_type=archive"
|
||||||
|
link_type: other
|
||||||
|
|
||||||
Refresh:
|
Refresh:
|
||||||
stage: refresh
|
stage: refresh
|
||||||
|
|||||||
@@ -4,15 +4,13 @@
|
|||||||
|
|
||||||
### Setup Snap.Hutao Project
|
### Setup Snap.Hutao Project
|
||||||
|
|
||||||
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/).
|
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/)
|
||||||
- No need to select workloads; Visual Studio will handle it automatically.
|
2. Open Visual Studio Installer to complete Visual Studio installation
|
||||||
- Close Visual Studio Installer to ensure a smooth installation experience for workloads.
|
- You need to install `.NET desktop development`, `Desktop development with C++` and `Universal Windows Platform development` components
|
||||||
- If using Visual Studio 2022 17.9 preview, skip step 5, as automatic extension installation is supported in this version.
|
3. Install `Single-project MSIX Packaging Tools for VS 2022` provided by Microsoft in Visual Studio marketplace
|
||||||
2. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device.
|
4. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device
|
||||||
3. Switch to the`develop` branch using git.
|
5. Switch git branch to `develop`
|
||||||
4. Open the project solution with your Visual Studio. Visual Studio will prompt you to install the necessary workloads, closing and reopening automatically.
|
6. Open project solution with your Visual Studio and then you are ready to go
|
||||||
5. (For Visual Studio 2022 17.8) Install the [Single-project MSIX Packaging Tools for VS 2022](https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17) provided by Microsoft in Visual Studio marketplace.
|
|
||||||
6. Open the project solution with your Visual Studio, and you are ready to go.
|
|
||||||
|
|
||||||
### Start Pull Request
|
### Start Pull Request
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ dotnet_diagnostic.SA1623.severity = none
|
|||||||
# SA1636: File header copyright text should match
|
# SA1636: File header copyright text should match
|
||||||
dotnet_diagnostic.SA1636.severity = none
|
dotnet_diagnostic.SA1636.severity = none
|
||||||
|
|
||||||
|
# SA1414: Tuple types in signatures should have element names
|
||||||
|
dotnet_diagnostic.SA1414.severity = none
|
||||||
|
|
||||||
# SA0001: XML comment analysis disabled
|
# SA0001: XML comment analysis disabled
|
||||||
dotnet_diagnostic.SA0001.severity = none
|
dotnet_diagnostic.SA0001.severity = none
|
||||||
csharp_style_prefer_parameter_null_checking = true:suggestion
|
csharp_style_prefer_parameter_null_checking = true:suggestion
|
||||||
@@ -322,6 +325,7 @@ dotnet_diagnostic.CA2227.severity = suggestion
|
|||||||
# CA2251: 使用 “string.Equals”
|
# CA2251: 使用 “string.Equals”
|
||||||
dotnet_diagnostic.CA2251.severity = suggestion
|
dotnet_diagnostic.CA2251.severity = suggestion
|
||||||
csharp_style_prefer_primary_constructors = true:suggestion
|
csharp_style_prefer_primary_constructors = true:suggestion
|
||||||
|
dotnet_diagnostic.SA1010.severity = none
|
||||||
|
|
||||||
[*.vb]
|
[*.vb]
|
||||||
#### 命名样式 ####
|
#### 命名样式 ####
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1.0",
|
|
||||||
"components": [
|
|
||||||
"Microsoft.VisualStudio.Workload.ManagedDesktop",
|
|
||||||
"Microsoft.VisualStudio.Workload.NativeDesktop",
|
|
||||||
"Microsoft.VisualStudio.Workload.Universal"
|
|
||||||
],
|
|
||||||
"extensions": [
|
|
||||||
"https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,4 @@
|
|||||||
// ADVAPI32
|
// COMCTL32
|
||||||
RegCloseKey
|
|
||||||
RegOpenKeyExW
|
|
||||||
RegNotifyChangeKeyValue
|
|
||||||
REG_NOTIFY_FILTER
|
|
||||||
HKEY_CLASSES_ROOT
|
|
||||||
HKEY_CURRENT_USER
|
|
||||||
HKEY_LOCAL_MACHINE
|
|
||||||
HKEY_USERS
|
|
||||||
HKEY_CURRENT_CONFIG
|
|
||||||
|
|
||||||
// COMCTL32
|
|
||||||
DefSubclassProc
|
DefSubclassProc
|
||||||
RemoveWindowSubclass
|
RemoveWindowSubclass
|
||||||
SetWindowSubclass
|
SetWindowSubclass
|
||||||
@@ -58,14 +47,12 @@ GetCursorPos
|
|||||||
GetDC
|
GetDC
|
||||||
GetDpiForWindow
|
GetDpiForWindow
|
||||||
GetForegroundWindow
|
GetForegroundWindow
|
||||||
GetWindowLongPtrW
|
|
||||||
GetWindowPlacement
|
GetWindowPlacement
|
||||||
GetWindowThreadProcessId
|
GetWindowThreadProcessId
|
||||||
ReleaseDC
|
ReleaseDC
|
||||||
RegisterHotKey
|
RegisterHotKey
|
||||||
SendInput
|
SendInput
|
||||||
SetForegroundWindow
|
SetForegroundWindow
|
||||||
SetWindowLongPtrW
|
|
||||||
UnregisterHotKey
|
UnregisterHotKey
|
||||||
|
|
||||||
// COM
|
// COM
|
||||||
@@ -87,7 +74,6 @@ E_FAIL
|
|||||||
INFINITE
|
INFINITE
|
||||||
RPC_E_WRONG_THREAD
|
RPC_E_WRONG_THREAD
|
||||||
MAX_PATH
|
MAX_PATH
|
||||||
WM_ERASEBKGND
|
|
||||||
WM_GETMINMAXINFO
|
WM_GETMINMAXINFO
|
||||||
WM_HOTKEY
|
WM_HOTKEY
|
||||||
WM_NCRBUTTONDOWN
|
WM_NCRBUTTONDOWN
|
||||||
@@ -103,7 +89,6 @@ LPTHREAD_START_ROUTINE
|
|||||||
|
|
||||||
// UI.WindowsAndMessaging
|
// UI.WindowsAndMessaging
|
||||||
MINMAXINFO
|
MINMAXINFO
|
||||||
WINDOW_EX_STYLE
|
|
||||||
|
|
||||||
// System.Com
|
// System.Com
|
||||||
CWMO_FLAGS
|
CWMO_FLAGS
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
|
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
|
||||||
<ImplicitUsings>disable</ImplicitUsings>
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
72
src/Snap.Hutao/Snap.Hutao.Win32/WinRTCustomMarshaler.cs
Normal file
72
src/Snap.Hutao/Snap.Hutao.Win32/WinRTCustomMarshaler.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Windows.Win32.CsWin32.InteropServices;
|
||||||
|
|
||||||
|
internal class WinRTCustomMarshaler : ICustomMarshaler
|
||||||
|
{
|
||||||
|
private static readonly string? AssemblyFullName = typeof(Windows.Foundation.IMemoryBuffer).Assembly.FullName;
|
||||||
|
|
||||||
|
private readonly string className;
|
||||||
|
private bool lookedForFromAbi;
|
||||||
|
private MethodInfo? fromAbiMethod;
|
||||||
|
|
||||||
|
private WinRTCustomMarshaler(string className)
|
||||||
|
{
|
||||||
|
this.className = className;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ICustomMarshaler GetInstance(string cookie)
|
||||||
|
{
|
||||||
|
return new WinRTCustomMarshaler(cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanUpManagedData(object ManagedObj)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanUpNativeData(nint pNativeData)
|
||||||
|
{
|
||||||
|
Marshal.Release(pNativeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetNativeDataSize()
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public nint MarshalManagedToNative(object ManagedObj)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public object MarshalNativeToManaged(nint thisPtr)
|
||||||
|
{
|
||||||
|
return className switch
|
||||||
|
{
|
||||||
|
"Windows.System.DispatcherQueueController" => Windows.System.DispatcherQueueController.FromAbi(thisPtr),
|
||||||
|
_ => MarshalNativeToManagedSlow(thisPtr),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private object MarshalNativeToManagedSlow(nint pNativeData)
|
||||||
|
{
|
||||||
|
if (!lookedForFromAbi)
|
||||||
|
{
|
||||||
|
Type? type = Type.GetType($"{className}, {AssemblyFullName}");
|
||||||
|
|
||||||
|
fromAbiMethod = type?.GetMethod("FromAbi");
|
||||||
|
lookedForFromAbi = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromAbiMethod is not null)
|
||||||
|
{
|
||||||
|
return fromAbiMethod.Invoke(default, new object[] { pNativeData })!;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Marshal.GetObjectForIUnknown(pNativeData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ EndProject
|
|||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9A95A964-04B1-477A-BDE7-505525B3CAD8}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9A95A964-04B1-477A-BDE7-505525B3CAD8}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
.editorconfig = .editorconfig
|
.editorconfig = .editorconfig
|
||||||
.vsconfig = .vsconfig
|
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
|
||||||
@@ -88,11 +87,11 @@ Global
|
|||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationLead"]}
|
|
||||||
RESX_ShowErrorsInErrorList = False
|
|
||||||
RESX_SortFileContentOnSave = True
|
|
||||||
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
|
|
||||||
RESX_NeutralResourcesLanguage = zh-CN
|
|
||||||
RESX_AutoApplyExistingTranslations = False
|
RESX_AutoApplyExistingTranslations = False
|
||||||
|
RESX_NeutralResourcesLanguage = zh-CN
|
||||||
|
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
|
||||||
|
RESX_SortFileContentOnSave = True
|
||||||
|
RESX_ShowErrorsInErrorList = False
|
||||||
|
RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationLead"]}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ namespace Snap.Hutao.Control;
|
|||||||
|
|
||||||
[TemplateVisualState(Name = "LoadingIn", GroupName = "CommonStates")]
|
[TemplateVisualState(Name = "LoadingIn", GroupName = "CommonStates")]
|
||||||
[TemplateVisualState(Name = "LoadingOut", GroupName = "CommonStates")]
|
[TemplateVisualState(Name = "LoadingOut", GroupName = "CommonStates")]
|
||||||
[TemplatePart(Name = "ContentGrid", Type = typeof(FrameworkElement))]
|
|
||||||
internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
|
internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
|
||||||
{
|
{
|
||||||
public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register(nameof(IsLoading), typeof(bool), typeof(Loading), new PropertyMetadata(default(bool), IsLoadingPropertyChanged));
|
public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register(nameof(IsLoading), typeof(bool), typeof(Loading), new PropertyMetadata(default(bool), IsLoadingPropertyChanged));
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:shc="using:Snap.Hutao.Control">
|
xmlns:shc="using:Snap.Hutao.Control">
|
||||||
|
|
||||||
<Style BasedOn="{StaticResource DefaultLoadingStyle}" TargetType="shc:Loading"/>
|
<Style TargetType="shc:Loading">
|
||||||
|
|
||||||
<Style x:Key="DefaultLoadingStyle" TargetType="shc:Loading">
|
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||||
|
|||||||
@@ -18,15 +18,12 @@ internal sealed class FontIconExtension : MarkupExtension
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Glyph { get; set; } = default!;
|
public string Glyph { get; set; } = default!;
|
||||||
|
|
||||||
public double FontSize { get; set; } = 12;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override object ProvideValue()
|
protected override object ProvideValue()
|
||||||
{
|
{
|
||||||
return new FontIcon()
|
return new FontIcon()
|
||||||
{
|
{
|
||||||
Glyph = Glyph,
|
Glyph = Glyph,
|
||||||
FontSize = FontSize,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,9 +20,6 @@
|
|||||||
<ItemsPanelTemplate x:Key="StackPanelSpacing4Template">
|
<ItemsPanelTemplate x:Key="StackPanelSpacing4Template">
|
||||||
<StackPanel Spacing="4"/>
|
<StackPanel Spacing="4"/>
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
<ItemsPanelTemplate x:Key="StackPanelSpacing8Template">
|
|
||||||
<StackPanel Spacing="8"/>
|
|
||||||
</ItemsPanelTemplate>
|
|
||||||
<ItemsPanelTemplate x:Key="UniformGridColumns2Spacing2Template">
|
<ItemsPanelTemplate x:Key="UniformGridColumns2Spacing2Template">
|
||||||
<cwcont:UniformGrid
|
<cwcont:UniformGrid
|
||||||
ColumnSpacing="2"
|
ColumnSpacing="2"
|
||||||
|
|||||||
@@ -6,12 +6,6 @@
|
|||||||
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</x:Double>
|
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</x:Double>
|
||||||
|
|
||||||
<x:Double x:Key="SettingsCardContentControlMinWidth">120</x:Double>
|
<x:Double x:Key="SettingsCardContentControlMinWidth">120</x:Double>
|
||||||
<x:Double x:Key="SettingsCardContentControlMinWidth2">160</x:Double>
|
|
||||||
|
|
||||||
<x:Double x:Key="SettingsCardContentControlSpacing">10</x:Double>
|
|
||||||
|
|
||||||
<Thickness x:Key="SettingsCardAlignSettingsExpanderPadding">16,16,44,16</Thickness>
|
|
||||||
<Thickness x:Key="SettingsExpanderItemHasIconPadding">16,8,16,8</Thickness>
|
|
||||||
|
|
||||||
<Style
|
<Style
|
||||||
x:Key="SettingsSectionHeaderTextBlockStyle"
|
x:Key="SettingsSectionHeaderTextBlockStyle"
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
|
|
||||||
<!-- EmotionIcon -->
|
<!-- EmotionIcon -->
|
||||||
<x:String x:Key="UI_EmotionIcon25">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png</x:String>
|
<x:String x:Key="UI_EmotionIcon25">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon52">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon52.png</x:String>
|
|
||||||
<x:String x:Key="UI_EmotionIcon71">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
|
<x:String x:Key="UI_EmotionIcon71">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
|
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
|
||||||
<x:String x:Key="UI_EmotionIcon271">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
|
<x:String x:Key="UI_EmotionIcon271">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Snap.Hutao.Core.IO.Http.DynamicProxy;
|
|
||||||
using Snap.Hutao.Core.Logging;
|
using Snap.Hutao.Core.Logging;
|
||||||
using Snap.Hutao.Service;
|
using Snap.Hutao.Service;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Windows.Globalization;
|
using Windows.Globalization;
|
||||||
|
|
||||||
@@ -43,7 +41,6 @@ internal static class DependencyInjection
|
|||||||
|
|
||||||
serviceProvider.InitializeConsoleWindow();
|
serviceProvider.InitializeConsoleWindow();
|
||||||
serviceProvider.InitializeCulture();
|
serviceProvider.InitializeCulture();
|
||||||
serviceProvider.InitializedDynamicHttpProxy();
|
|
||||||
|
|
||||||
return serviceProvider;
|
return serviceProvider;
|
||||||
}
|
}
|
||||||
@@ -51,10 +48,10 @@ internal static class DependencyInjection
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static void InitializeCulture(this IServiceProvider serviceProvider)
|
private static void InitializeCulture(this IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
CultureOptions cultureOptions = serviceProvider.GetRequiredService<CultureOptions>();
|
AppOptions appOptions = serviceProvider.GetRequiredService<AppOptions>();
|
||||||
cultureOptions.SystemCulture = CultureInfo.CurrentCulture;
|
appOptions.PreviousCulture = CultureInfo.CurrentCulture;
|
||||||
|
|
||||||
CultureInfo cultureInfo = cultureOptions.CurrentCulture;
|
CultureInfo cultureInfo = appOptions.CurrentCulture;
|
||||||
|
|
||||||
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
|
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
|
||||||
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
||||||
@@ -70,9 +67,4 @@ internal static class DependencyInjection
|
|||||||
{
|
{
|
||||||
_ = serviceProvider.GetRequiredService<ConsoleWindowLifeTime>();
|
_ = serviceProvider.GetRequiredService<ConsoleWindowLifeTime>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void InitializedDynamicHttpProxy(this IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
HttpClient.DefaultProxy = serviceProvider.GetRequiredService<DynamicHttpProxy>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -38,8 +38,8 @@ internal static class IocConfiguration
|
|||||||
|
|
||||||
private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
|
private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
|
||||||
{
|
{
|
||||||
RuntimeOptions runtimeOptions = provider.GetRequiredService<RuntimeOptions>();
|
RuntimeOptions hutaoOptions = provider.GetRequiredService<RuntimeOptions>();
|
||||||
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
|
string dbFile = System.IO.Path.Combine(hutaoOptions.DataFolder, "Userdata.db");
|
||||||
string sqlConnectionString = $"Data Source={dbFile}";
|
string sqlConnectionString = $"Data Source={dbFile}";
|
||||||
|
|
||||||
// Temporarily create a context
|
// Temporarily create a context
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ internal static partial class IocHttpClientConfiguration
|
|||||||
/// <param name="client">配置后的客户端</param>
|
/// <param name="client">配置后的客户端</param>
|
||||||
private static void DefaultConfiguration(IServiceProvider serviceProvider, HttpClient client)
|
private static void DefaultConfiguration(IServiceProvider serviceProvider, HttpClient client)
|
||||||
{
|
{
|
||||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||||
|
|
||||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(runtimeOptions.UserAgent);
|
client.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Win32.Registry;
|
|
||||||
using System.Net;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.IO.Http.DynamicProxy;
|
|
||||||
|
|
||||||
[Injection(InjectAs.Singleton)]
|
|
||||||
internal sealed partial class DynamicHttpProxy : IWebProxy, IDisposable
|
|
||||||
{
|
|
||||||
private const string ProxySettingPath = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections";
|
|
||||||
|
|
||||||
private static readonly MethodInfo ConstructSystemProxyMethod;
|
|
||||||
|
|
||||||
private readonly RegistryWatcher watcher;
|
|
||||||
|
|
||||||
private IWebProxy innerProxy = default!;
|
|
||||||
|
|
||||||
static DynamicHttpProxy()
|
|
||||||
{
|
|
||||||
Type? systemProxyInfoType = typeof(System.Net.Http.SocketsHttpHandler).Assembly.GetType("System.Net.Http.SystemProxyInfo");
|
|
||||||
ArgumentNullException.ThrowIfNull(systemProxyInfoType);
|
|
||||||
|
|
||||||
MethodInfo? constructSystemProxyMethod = systemProxyInfoType.GetMethod("ConstructSystemProxy", BindingFlags.Static | BindingFlags.Public);
|
|
||||||
ArgumentNullException.ThrowIfNull(constructSystemProxyMethod);
|
|
||||||
ConstructSystemProxyMethod = constructSystemProxyMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DynamicHttpProxy()
|
|
||||||
{
|
|
||||||
UpdateProxy();
|
|
||||||
|
|
||||||
watcher = new(ProxySettingPath, UpdateProxy);
|
|
||||||
watcher.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public ICredentials? Credentials
|
|
||||||
{
|
|
||||||
get => InnerProxy.Credentials;
|
|
||||||
set => InnerProxy.Credentials = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IWebProxy InnerProxy
|
|
||||||
{
|
|
||||||
get => innerProxy;
|
|
||||||
|
|
||||||
[MemberNotNull(nameof(innerProxy))]
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(innerProxy, value))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(innerProxy as IDisposable)?.Dispose();
|
|
||||||
innerProxy = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[MemberNotNull(nameof(innerProxy))]
|
|
||||||
public void UpdateProxy()
|
|
||||||
{
|
|
||||||
IWebProxy? proxy = ConstructSystemProxyMethod.Invoke(default, default) as IWebProxy;
|
|
||||||
ArgumentNullException.ThrowIfNull(proxy);
|
|
||||||
|
|
||||||
InnerProxy = proxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uri? GetProxy(Uri destination)
|
|
||||||
{
|
|
||||||
return InnerProxy.GetProxy(destination);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsBypassed(Uri host)
|
|
||||||
{
|
|
||||||
return InnerProxy.IsBypassed(host);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
(innerProxy as IDisposable)?.Dispose();
|
|
||||||
watcher.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,14 +11,11 @@ namespace Snap.Hutao.Core.IO.Ini;
|
|||||||
[HighQuality]
|
[HighQuality]
|
||||||
internal static class IniSerializer
|
internal static class IniSerializer
|
||||||
{
|
{
|
||||||
public static List<IniElement> DeserializeFromFile(string filePath)
|
/// <summary>
|
||||||
{
|
/// 反序列化
|
||||||
using (FileStream readStream = File.OpenRead(filePath))
|
/// </summary>
|
||||||
{
|
/// <param name="fileStream">文件流</param>
|
||||||
return Deserialize(readStream);
|
/// <returns>Ini 元素集合</returns>
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<IniElement> Deserialize(FileStream fileStream)
|
public static List<IniElement> Deserialize(FileStream fileStream)
|
||||||
{
|
{
|
||||||
List<IniElement> results = [];
|
List<IniElement> results = [];
|
||||||
@@ -53,14 +50,11 @@ internal static class IniSerializer
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SerializeToFile(string filePath, IEnumerable<IniElement> elements)
|
/// <summary>
|
||||||
{
|
/// 序列化
|
||||||
using (FileStream writeStream = File.Create(filePath))
|
/// </summary>
|
||||||
{
|
/// <param name="fileStream">写入的流</param>
|
||||||
Serialize(writeStream, elements);
|
/// <param name="elements">元素</param>
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Serialize(FileStream fileStream, IEnumerable<IniElement> elements)
|
public static void Serialize(FileStream fileStream, IEnumerable<IniElement> elements)
|
||||||
{
|
{
|
||||||
using (StreamWriter writer = new(fileStream))
|
using (StreamWriter writer = new(fileStream))
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ internal sealed partial class Activation : IActivation
|
|||||||
|
|
||||||
serviceProvider
|
serviceProvider
|
||||||
.GetRequiredService<IDiscordService>()
|
.GetRequiredService<IDiscordService>()
|
||||||
.SetNormalActivityAsync()
|
.SetNormalActivity()
|
||||||
.SafeForget();
|
.SafeForget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,11 +39,6 @@ internal sealed class ScheduleTaskInterop : IScheduleTaskInterop
|
|||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
if (WScriptExists(DailyNoteRefreshScriptName, out string fullPath))
|
|
||||||
{
|
|
||||||
File.Delete(fullPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,9 +69,4 @@ internal static class StructMarshal
|
|||||||
{
|
{
|
||||||
return new(point.X, point.Y, size.Width, size.Height);
|
return new(point.X, point.Y, size.Width, size.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SizeInt32 SizeInt32(RectInt32 rect)
|
|
||||||
{
|
|
||||||
return new(rect.Width, rect.Height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -8,12 +8,12 @@ internal readonly struct Delay
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 随机延迟
|
/// 随机延迟
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="min">最小,闭</param>
|
/// <param name="minMilliSeconds">最小,闭</param>
|
||||||
/// <param name="max">最小,开</param>
|
/// <param name="maxMilliSeconds">最小,开</param>
|
||||||
/// <returns>任务</returns>
|
/// <returns>任务</returns>
|
||||||
public static ValueTask RandomMilliSeconds(int min, int max)
|
public static ValueTask Random(int minMilliSeconds, int maxMilliSeconds)
|
||||||
{
|
{
|
||||||
return Task.Delay((int)(System.Random.Shared.NextDouble() * (max - min)) + min).AsValueTask();
|
return Task.Delay((int)(System.Random.Shared.NextDouble() * (maxMilliSeconds - minMilliSeconds)) + minMilliSeconds).AsValueTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ValueTask FromSeconds(int seconds)
|
public static ValueTask FromSeconds(int seconds)
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Microsoft.UI;
|
|
||||||
using Microsoft.UI.Composition;
|
|
||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Media;
|
|
||||||
using Windows.UI;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.Windowing.Backdrop;
|
|
||||||
|
|
||||||
internal sealed class TransparentBackdrop : SystemBackdrop, IDisposable, IBackdropNeedEraseBackground
|
|
||||||
{
|
|
||||||
private readonly object compositorLock = new();
|
|
||||||
|
|
||||||
private Color tintColor;
|
|
||||||
private Windows.UI.Composition.CompositionColorBrush? brush;
|
|
||||||
private Windows.UI.Composition.Compositor? compositor;
|
|
||||||
|
|
||||||
public TransparentBackdrop()
|
|
||||||
: this(Colors.Transparent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public TransparentBackdrop(Color tintColor)
|
|
||||||
{
|
|
||||||
this.tintColor = tintColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal Windows.UI.Composition.Compositor Compositor
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (compositor is null)
|
|
||||||
{
|
|
||||||
lock (compositorLock)
|
|
||||||
{
|
|
||||||
if (compositor is null)
|
|
||||||
{
|
|
||||||
DispatcherQueue.EnsureSystemDispatcherQueue();
|
|
||||||
compositor = new Windows.UI.Composition.Compositor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return compositor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
compositor?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot)
|
|
||||||
{
|
|
||||||
brush ??= Compositor.CreateColorBrush(tintColor);
|
|
||||||
connectedTarget.SystemBackdrop = brush;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnTargetDisconnected(ICompositionSupportsSystemBackdrop disconnectedTarget)
|
|
||||||
{
|
|
||||||
disconnectedTarget.SystemBackdrop = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface IBackdropNeedEraseBackground;
|
|
||||||
@@ -9,8 +9,6 @@ namespace Snap.Hutao.Core.Windowing;
|
|||||||
[HighQuality]
|
[HighQuality]
|
||||||
internal enum BackdropType
|
internal enum BackdropType
|
||||||
{
|
{
|
||||||
Transparent = -1,
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 无
|
/// 无
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ using Microsoft.UI.Xaml.Media;
|
|||||||
using Snap.Hutao.Core.LifeCycle;
|
using Snap.Hutao.Core.LifeCycle;
|
||||||
using Snap.Hutao.Core.Setting;
|
using Snap.Hutao.Core.Setting;
|
||||||
using Snap.Hutao.Service;
|
using Snap.Hutao.Service;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Windows.Graphics;
|
using Windows.Graphics;
|
||||||
using Windows.UI;
|
using Windows.UI;
|
||||||
@@ -54,10 +53,10 @@ internal sealed class WindowController
|
|||||||
|
|
||||||
private void InitializeCore()
|
private void InitializeCore()
|
||||||
{
|
{
|
||||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||||
|
|
||||||
window.AppWindow.Title = SH.FormatAppNameAndVersion(runtimeOptions.Version);
|
window.AppWindow.Title = SH.FormatAppNameAndVersion(hutaoOptions.Version);
|
||||||
window.AppWindow.SetIcon(Path.Combine(runtimeOptions.InstalledLocation, "Assets/Logo.ico"));
|
window.AppWindow.SetIcon(Path.Combine(hutaoOptions.InstalledLocation, "Assets/Logo.ico"));
|
||||||
ExtendsContentIntoTitleBar();
|
ExtendsContentIntoTitleBar();
|
||||||
|
|
||||||
RecoverOrInitWindowSize();
|
RecoverOrInitWindowSize();
|
||||||
@@ -158,7 +157,6 @@ internal sealed class WindowController
|
|||||||
{
|
{
|
||||||
window.SystemBackdrop = backdropType switch
|
window.SystemBackdrop = backdropType switch
|
||||||
{
|
{
|
||||||
BackdropType.Transparent => new Backdrop.TransparentBackdrop(),
|
|
||||||
BackdropType.MicaAlt => new MicaBackdrop() { Kind = MicaKind.BaseAlt },
|
BackdropType.MicaAlt => new MicaBackdrop() { Kind = MicaKind.BaseAlt },
|
||||||
BackdropType.Mica => new MicaBackdrop() { Kind = MicaKind.Base },
|
BackdropType.Mica => new MicaBackdrop() { Kind = MicaKind.Base },
|
||||||
BackdropType.Acrylic => new DesktopAcrylicBackdrop(),
|
BackdropType.Acrylic => new DesktopAcrylicBackdrop(),
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
|
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Windows.Win32.Foundation;
|
|
||||||
using Windows.Win32.UI.WindowsAndMessaging;
|
|
||||||
using WinRT.Interop;
|
|
||||||
using static Windows.Win32.PInvoke;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.Windowing;
|
namespace Snap.Hutao.Core.Windowing;
|
||||||
|
|
||||||
@@ -20,12 +16,4 @@ internal static class WindowExtension
|
|||||||
WindowController windowController = new(window, window.WindowOptions, serviceProvider);
|
WindowController windowController = new(window, window.WindowOptions, serviceProvider);
|
||||||
WindowControllers.Add(window, windowController);
|
WindowControllers.Add(window, windowController);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetLayeredWindow(this Window window)
|
|
||||||
{
|
|
||||||
HWND hwnd = (HWND)WindowNative.GetWindowHandle(window);
|
|
||||||
nint style = GetWindowLongPtr(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
|
|
||||||
style |= (nint)WINDOW_EX_STYLE.WS_EX_LAYERED;
|
|
||||||
SetWindowLongPtr(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, style);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Snap.Hutao.Core.Windowing.Backdrop;
|
|
||||||
using Snap.Hutao.Core.Windowing.HotKey;
|
using Snap.Hutao.Core.Windowing.HotKey;
|
||||||
using Windows.Win32.Foundation;
|
using Windows.Win32.Foundation;
|
||||||
using Windows.Win32.UI.Shell;
|
using Windows.Win32.UI.Shell;
|
||||||
@@ -111,16 +110,6 @@ internal sealed class WindowSubclass : IDisposable
|
|||||||
hotKeyController.OnHotKeyPressed(*(HotKeyParameter*)&lParam);
|
hotKeyController.OnHotKeyPressed(*(HotKeyParameter*)&lParam);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case WM_ERASEBKGND:
|
|
||||||
{
|
|
||||||
if (window.SystemBackdrop is IBackdropNeedEraseBackground)
|
|
||||||
{
|
|
||||||
return (LRESULT)(int)(BOOL)true;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return DefSubclassProc(hwnd, uMsg, wParam, lParam);
|
return DefSubclassProc(hwnd, uMsg, wParam, lParam);
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<Window
|
|
||||||
x:Class="Snap.Hutao.IdentifyMonitorWindow"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
|
||||||
mc:Ignorable="d">
|
|
||||||
<StackPanel
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Spacing="3">
|
|
||||||
<TextBlock Text="{shcm:ResourceString Name=WindowIdentifyMonitorHeader}"/>
|
|
||||||
<TextBlock
|
|
||||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
|
||||||
Text="{x:Bind Monitor}"
|
|
||||||
TextAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Window>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Microsoft.UI.Windowing;
|
|
||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Snap.Hutao.Core;
|
|
||||||
using Windows.Graphics;
|
|
||||||
|
|
||||||
namespace Snap.Hutao;
|
|
||||||
|
|
||||||
internal sealed partial class IdentifyMonitorWindow : Window
|
|
||||||
{
|
|
||||||
public IdentifyMonitorWindow(DisplayArea displayArea, int index)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
Monitor = $"{displayArea.DisplayId.Value:X8}:{index}";
|
|
||||||
|
|
||||||
OverlappedPresenter presenter = OverlappedPresenter.Create();
|
|
||||||
presenter.SetBorderAndTitleBar(false, false);
|
|
||||||
presenter.IsAlwaysOnTop = true;
|
|
||||||
presenter.IsResizable = false;
|
|
||||||
AppWindow.SetPresenter(presenter);
|
|
||||||
|
|
||||||
PointInt32 point = new(40, 32);
|
|
||||||
SizeInt32 size = StructMarshal.SizeInt32(displayArea.WorkArea).Scale(0.1);
|
|
||||||
AppWindow.MoveAndResize(StructMarshal.RectInt32(point, size), displayArea);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Monitor { get; private set; }
|
|
||||||
}
|
|
||||||
@@ -11,12 +11,6 @@ internal static class CollectionsNameValue
|
|||||||
return [.. Enum.GetValues<TEnum>().Select(x => new NameValue<TEnum>(x.ToString(), x))];
|
return [.. Enum.GetValues<TEnum>().Select(x => new NameValue<TEnum>(x.ToString(), x))];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<NameValue<TEnum>> FromEnum<TEnum>(Func<TEnum, bool> codiction)
|
|
||||||
where TEnum : struct, Enum
|
|
||||||
{
|
|
||||||
return [.. Enum.GetValues<TEnum>().Where(codiction).Select(x => new NameValue<TEnum>(x.ToString(), x))];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<NameValue<TSource>> From<TSource>(IEnumerable<TSource> sources, Func<TSource, string> nameSelector)
|
public static List<NameValue<TSource>> From<TSource>(IEnumerable<TSource> sources, Func<TSource, string> nameSelector)
|
||||||
{
|
{
|
||||||
return [.. sources.Select(x => new NameValue<TSource>(nameSelector(x), x))];
|
return [.. sources.Select(x => new NameValue<TSource>(nameSelector(x), x))];
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using Snap.Hutao.Core;
|
using Snap.Hutao.Core;
|
||||||
using Snap.Hutao.Core.Abstraction;
|
using Snap.Hutao.Core.Abstraction;
|
||||||
using Snap.Hutao.Service;
|
|
||||||
using Snap.Hutao.Service.Metadata;
|
using Snap.Hutao.Service.Metadata;
|
||||||
using Snap.Hutao.Web.Hoyolab;
|
using Snap.Hutao.Web.Hoyolab;
|
||||||
|
|
||||||
@@ -13,7 +12,7 @@ namespace Snap.Hutao.Model.InterChange.GachaLog;
|
|||||||
/// UIGF格式的信息
|
/// UIGF格式的信息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HighQuality]
|
[HighQuality]
|
||||||
internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, CultureOptions, string>
|
internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, MetadataOptions, string>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户Uid
|
/// 用户Uid
|
||||||
@@ -66,12 +65,12 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, CultureO
|
|||||||
[JsonPropertyName("region_time_zone")]
|
[JsonPropertyName("region_time_zone")]
|
||||||
public int? RegionTimeZone { get; set; } = default!;
|
public int? RegionTimeZone { get; set; } = default!;
|
||||||
|
|
||||||
public static UIGFInfo From(RuntimeOptions runtimeOptions, CultureOptions cultureOptions, string uid)
|
public static UIGFInfo From(RuntimeOptions runtimeOptions, MetadataOptions metadataOptions, string uid)
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
Uid = uid,
|
Uid = uid,
|
||||||
Language = cultureOptions.LanguageCode,
|
Language = metadataOptions.LanguageCode,
|
||||||
ExportTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
ExportTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||||
ExportApp = SH.AppName,
|
ExportApp = SH.AppName,
|
||||||
ExportAppVersion = runtimeOptions.Version.ToString(),
|
ExportAppVersion = runtimeOptions.Version.ToString(),
|
||||||
|
|||||||
@@ -64,14 +64,14 @@ internal sealed class UIIFInfo
|
|||||||
/// <returns>专用 UIGF 信息</returns>
|
/// <returns>专用 UIGF 信息</returns>
|
||||||
public static UIIFInfo From(IServiceProvider serviceProvider, string uid)
|
public static UIIFInfo From(IServiceProvider serviceProvider, string uid)
|
||||||
{
|
{
|
||||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
Uid = uid,
|
Uid = uid,
|
||||||
ExportTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
ExportTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||||
ExportApp = SH.AppName,
|
ExportApp = SH.AppName,
|
||||||
ExportAppVersion = runtimeOptions.Version.ToString(),
|
ExportAppVersion = hutaoOptions.Version.ToString(),
|
||||||
UIIFVersion = UIIF.CurrentVersion,
|
UIIFVersion = UIIF.CurrentVersion,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -870,23 +870,11 @@
|
|||||||
<value>文件系统权限不足,无法转换服务器</value>
|
<value>文件系统权限不足,无法转换服务器</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ServiceGameEnsureGameResourceQueryResourceInformation" xml:space="preserve">
|
<data name="ServiceGameEnsureGameResourceQueryResourceInformation" xml:space="preserve">
|
||||||
<value>下载游戏资源索引</value>
|
<value>查询游戏资源信息</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ServiceGameFileOperationExceptionMessage" xml:space="preserve">
|
<data name="ServiceGameFileOperationExceptionMessage" xml:space="preserve">
|
||||||
<value>游戏文件操作失败:{0}</value>
|
<value>游戏文件操作失败:{0}</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ServiceGameLaunchExecutionGameFpsUnlockFailed" xml:space="preserve">
|
|
||||||
<value>解锁帧率上限失败</value>
|
|
||||||
</data>
|
|
||||||
<data name="ServiceGameLaunchExecutionGameIsRunning" xml:space="preserve">
|
|
||||||
<value>游戏进程运行中</value>
|
|
||||||
</data>
|
|
||||||
<data name="ServiceGameLaunchExecutionGamePathNotValid" xml:space="preserve">
|
|
||||||
<value>请选择游戏路径</value>
|
|
||||||
</data>
|
|
||||||
<data name="ServiceGameLaunchExecutionGameResourceQueryIndexFailed" xml:space="preserve">
|
|
||||||
<value>下载游戏资源索引失败: {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="ServiceGameLaunchPhaseProcessExited" xml:space="preserve">
|
<data name="ServiceGameLaunchPhaseProcessExited" xml:space="preserve">
|
||||||
<value>游戏进程已退出</value>
|
<value>游戏进程已退出</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1310,9 +1298,6 @@
|
|||||||
<data name="ViewDialogUserTitle" xml:space="preserve">
|
<data name="ViewDialogUserTitle" xml:space="preserve">
|
||||||
<value>设置 Cookie</value>
|
<value>设置 Cookie</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ViewFeedbackHeader" xml:space="preserve">
|
|
||||||
<value>反馈中心</value>
|
|
||||||
</data>
|
|
||||||
<data name="ViewGachaLogHeader" xml:space="preserve">
|
<data name="ViewGachaLogHeader" xml:space="preserve">
|
||||||
<value>祈愿记录</value>
|
<value>祈愿记录</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1562,9 +1547,6 @@
|
|||||||
<data name="ViewModelLaunchGameEnsureGameResourceFail" xml:space="preserve">
|
<data name="ViewModelLaunchGameEnsureGameResourceFail" xml:space="preserve">
|
||||||
<value>切换服务器失败</value>
|
<value>切换服务器失败</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ViewModelLaunchGameIdentifyMonitorsAction" xml:space="preserve">
|
|
||||||
<value>识别显示器</value>
|
|
||||||
</data>
|
|
||||||
<data name="ViewModelLaunchGameMultiChannelReadFail" xml:space="preserve">
|
<data name="ViewModelLaunchGameMultiChannelReadFail" xml:space="preserve">
|
||||||
<value>无法读取游戏配置文件: {0},可能是文件不存在或权限不足</value>
|
<value>无法读取游戏配置文件: {0},可能是文件不存在或权限不足</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1853,33 +1835,6 @@
|
|||||||
<data name="ViewPageDailyNoteVerify" xml:space="preserve">
|
<data name="ViewPageDailyNoteVerify" xml:space="preserve">
|
||||||
<value>验证当前用户与角色</value>
|
<value>验证当前用户与角色</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ViewPageFeedbackAutoSuggestBoxPlaceholder" xml:space="preserve">
|
|
||||||
<value>搜索问题与建议</value>
|
|
||||||
</data>
|
|
||||||
<data name="ViewPageFeedBackBasicInformation" xml:space="preserve">
|
|
||||||
<value>基本信息</value>
|
|
||||||
</data>
|
|
||||||
<data name="ViewPageFeedbackCommonLinksHeader" xml:space="preserve">
|
|
||||||
<value>常用链接</value>
|
|
||||||
</data>
|
|
||||||
<data name="ViewPageFeedbackEngageWithUsDescription" xml:space="preserve">
|
|
||||||
<value>与我们密切联系</value>
|
|
||||||
</data>
|
|
||||||
<data name="ViewPageFeedbackFeatureGuideHeader" xml:space="preserve">
|
|
||||||
<value>功能指南</value>
|
|
||||||
</data>
|
|
||||||
<data name="ViewPageFeedbackGithubIssuesDescription" xml:space="preserve">
|
|
||||||
<value>我们总是优先处理 Github 上的问题</value>
|
|
||||||
</data>
|
|
||||||
<data name="ViewPageFeedbackRoadmapDescription" xml:space="preserve">
|
|
||||||
<value>开发路线规划</value>
|
|
||||||
</data>
|
|
||||||
<data name="ViewPageFeedbackServerStatusDescription" xml:space="preserve">
|
|
||||||
<value>胡桃服务可用性监控</value>
|
|
||||||
</data>
|
|
||||||
<data name="ViewPageFeedbackServerStatusHeader" xml:space="preserve">
|
|
||||||
<value>胡桃服务</value>
|
|
||||||
</data>
|
|
||||||
<data name="ViewPageGachaLogAggressiveRefresh" xml:space="preserve">
|
<data name="ViewPageGachaLogAggressiveRefresh" xml:space="preserve">
|
||||||
<value>全量刷新</value>
|
<value>全量刷新</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2636,9 +2591,6 @@
|
|||||||
<data name="ViewSettingFolderViewOpenFolderAction" xml:space="preserve">
|
<data name="ViewSettingFolderViewOpenFolderAction" xml:space="preserve">
|
||||||
<value>打开文件夹</value>
|
<value>打开文件夹</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ViewSettingHeader" xml:space="preserve">
|
|
||||||
<value>设置</value>
|
|
||||||
</data>
|
|
||||||
<data name="ViewSpiralAbyssAvatarAppearanceRankDescription" xml:space="preserve">
|
<data name="ViewSpiralAbyssAvatarAppearanceRankDescription" xml:space="preserve">
|
||||||
<value>角色出场率 = 本层上阵该角色次数(层内重复出现只记一次)/ 深渊记录总数</value>
|
<value>角色出场率 = 本层上阵该角色次数(层内重复出现只记一次)/ 深渊记录总数</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -3002,7 +2954,4 @@
|
|||||||
<data name="WebResponseRequestExceptionFormat" xml:space="preserve">
|
<data name="WebResponseRequestExceptionFormat" xml:space="preserve">
|
||||||
<value>[{0}] 中的 [{1}] 网络请求异常,请稍后再试</value>
|
<value>[{0}] 中的 [{1}] 网络请求异常,请稍后再试</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="WindowIdentifyMonitorHeader" xml:space="preserve">
|
|
||||||
<value>显示器编号</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
</root>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -6,6 +6,7 @@ using Snap.Hutao.Model;
|
|||||||
using Snap.Hutao.Model.Entity;
|
using Snap.Hutao.Model.Entity;
|
||||||
using Snap.Hutao.Service.Abstraction;
|
using Snap.Hutao.Service.Abstraction;
|
||||||
using Snap.Hutao.Web.Hoyolab;
|
using Snap.Hutao.Web.Hoyolab;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service;
|
namespace Snap.Hutao.Service;
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ internal sealed partial class AppOptions : DbStoreOptions
|
|||||||
{
|
{
|
||||||
private bool? isEmptyHistoryWishVisible;
|
private bool? isEmptyHistoryWishVisible;
|
||||||
private BackdropType? backdropType;
|
private BackdropType? backdropType;
|
||||||
|
private CultureInfo? currentCulture;
|
||||||
private Region? region;
|
private Region? region;
|
||||||
private string? geetestCustomCompositeUrl;
|
private string? geetestCustomCompositeUrl;
|
||||||
|
|
||||||
@@ -24,7 +26,7 @@ internal sealed partial class AppOptions : DbStoreOptions
|
|||||||
set => SetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, value);
|
set => SetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<NameValue<BackdropType>> BackdropTypes { get; } = CollectionsNameValue.FromEnum<BackdropType>(type => type >= 0);
|
public List<NameValue<BackdropType>> BackdropTypes { get; } = CollectionsNameValue.FromEnum<BackdropType>();
|
||||||
|
|
||||||
public BackdropType BackdropType
|
public BackdropType BackdropType
|
||||||
{
|
{
|
||||||
@@ -32,6 +34,14 @@ internal sealed partial class AppOptions : DbStoreOptions
|
|||||||
set => SetOption(ref backdropType, SettingEntry.SystemBackdropType, value, value => value.ToStringOrEmpty());
|
set => SetOption(ref backdropType, SettingEntry.SystemBackdropType, value, value => value.ToStringOrEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<NameValue<CultureInfo>> Cultures { get; } = SupportedCultures.Get();
|
||||||
|
|
||||||
|
public CultureInfo CurrentCulture
|
||||||
|
{
|
||||||
|
get => GetOption(ref currentCulture, SettingEntry.Culture, CultureInfo.GetCultureInfo, CultureInfo.CurrentCulture);
|
||||||
|
set => SetOption(ref currentCulture, SettingEntry.Culture, value, value => value.Name);
|
||||||
|
}
|
||||||
|
|
||||||
public Lazy<List<NameValue<Region>>> LazyRegions { get; } = new(KnownRegions.Get);
|
public Lazy<List<NameValue<Region>>> LazyRegions { get; } = new(KnownRegions.Get);
|
||||||
|
|
||||||
public Region Region
|
public Region Region
|
||||||
@@ -45,4 +55,6 @@ internal sealed partial class AppOptions : DbStoreOptions
|
|||||||
get => GetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl);
|
get => GetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl);
|
||||||
set => SetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl, value);
|
set => SetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal CultureInfo PreviousCulture { get; set; } = default!;
|
||||||
}
|
}
|
||||||
@@ -3,11 +3,17 @@
|
|||||||
|
|
||||||
using Snap.Hutao.Model;
|
using Snap.Hutao.Model;
|
||||||
using Snap.Hutao.Web.Hoyolab;
|
using Snap.Hutao.Web.Hoyolab;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service;
|
namespace Snap.Hutao.Service;
|
||||||
|
|
||||||
internal static class AppOptionsExtension
|
internal static class AppOptionsExtension
|
||||||
{
|
{
|
||||||
|
public static NameValue<CultureInfo>? GetCurrentCultureForSelectionOrDefault(this AppOptions appOptions)
|
||||||
|
{
|
||||||
|
return appOptions.Cultures.SingleOrDefault(c => c.Value == appOptions.CurrentCulture);
|
||||||
|
}
|
||||||
|
|
||||||
public static NameValue<Region>? GetCurrentRegionForSelectionOrDefault(this AppOptions appOptions)
|
public static NameValue<Region>? GetCurrentRegionForSelectionOrDefault(this AppOptions appOptions)
|
||||||
{
|
{
|
||||||
return appOptions.LazyRegions.Value.SingleOrDefault(c => c.Value.Value == appOptions.Region.Value);
|
return appOptions.LazyRegions.Value.SingleOrDefault(c => c.Value.Value == appOptions.Region.Value);
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Model;
|
|
||||||
using Snap.Hutao.Model.Entity;
|
|
||||||
using Snap.Hutao.Service.Abstraction;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service;
|
|
||||||
|
|
||||||
[ConstructorGenerated(CallBaseConstructor = true)]
|
|
||||||
[Injection(InjectAs.Singleton)]
|
|
||||||
internal sealed partial class CultureOptions : DbStoreOptions
|
|
||||||
{
|
|
||||||
private CultureInfo? currentCulture;
|
|
||||||
private string? localeName;
|
|
||||||
private string? languageCode;
|
|
||||||
|
|
||||||
public List<NameValue<CultureInfo>> Cultures { get; } = SupportedCultures.Get();
|
|
||||||
|
|
||||||
public CultureInfo CurrentCulture
|
|
||||||
{
|
|
||||||
get => GetOption(ref currentCulture, SettingEntry.Culture, CultureInfo.GetCultureInfo, CultureInfo.CurrentCulture);
|
|
||||||
set => SetOption(ref currentCulture, SettingEntry.Culture, value, value => value.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CultureInfo SystemCulture { get; set; } = default!;
|
|
||||||
|
|
||||||
public string LocaleName { get => localeName ??= CultureOptionsExtension.GetLocaleName(CurrentCulture); }
|
|
||||||
|
|
||||||
public string LanguageCode
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (languageCode is null && !LocaleNames.TryGetLanguageCodeFromLocaleName(LocaleName, out languageCode))
|
|
||||||
{
|
|
||||||
throw new KeyNotFoundException($"Invalid localeName: '{LocaleName}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
return languageCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -58,11 +58,7 @@ internal sealed partial class DailyNoteOptions : DbStoreOptions
|
|||||||
{
|
{
|
||||||
if (SelectedRefreshTime is not null)
|
if (SelectedRefreshTime is not null)
|
||||||
{
|
{
|
||||||
if (!scheduleTaskInterop.RegisterForDailyNoteRefresh(SelectedRefreshTime.Value))
|
scheduleTaskInterop.RegisterForDailyNoteRefresh(SelectedRefreshTime.Value);
|
||||||
{
|
|
||||||
serviceProvider.GetRequiredService<IInfoBarService>().Warning(SH.ViewModelDailyNoteRegisterTaskFail);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -172,8 +172,6 @@ internal static class DiscordController
|
|||||||
|
|
||||||
private static async ValueTask DiscordRunCallbacksAsync(CancellationToken cancellationToken)
|
private static async ValueTask DiscordRunCallbacksAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
int notRunningCounter = 0;
|
|
||||||
|
|
||||||
using (PeriodicTimer timer = new(TimeSpan.FromMilliseconds(500)))
|
using (PeriodicTimer timer = new(TimeSpan.FromMilliseconds(500)))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -192,18 +190,7 @@ internal static class DiscordController
|
|||||||
DiscordResult result = DiscordCoreRunRunCallbacks();
|
DiscordResult result = DiscordCoreRunRunCallbacks();
|
||||||
if (result is not DiscordResult.Ok)
|
if (result is not DiscordResult.Ok)
|
||||||
{
|
{
|
||||||
if (result is DiscordResult.NotRunning)
|
System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK ERROR]:{result:D} {result}");
|
||||||
{
|
|
||||||
if (++notRunningCounter > 20)
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
notRunningCounter = 0;
|
|
||||||
System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK ERROR]:{result:D} {result}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (SEHException ex)
|
catch (SEHException ex)
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ internal sealed partial class DiscordService : IDiscordService, IDisposable
|
|||||||
{
|
{
|
||||||
private readonly RuntimeOptions runtimeOptions;
|
private readonly RuntimeOptions runtimeOptions;
|
||||||
|
|
||||||
public async ValueTask SetPlayingActivityAsync(bool isOversea)
|
public async ValueTask SetPlayingActivity(bool isOversea)
|
||||||
{
|
{
|
||||||
_ = isOversea
|
_ = isOversea
|
||||||
? await DiscordController.SetPlayingGenshinImpactAsync().ConfigureAwait(false)
|
? await DiscordController.SetPlayingGenshinImpactAsync().ConfigureAwait(false)
|
||||||
: await DiscordController.SetPlayingYuanShenAsync().ConfigureAwait(false);
|
: await DiscordController.SetPlayingYuanShenAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask SetNormalActivityAsync()
|
public async ValueTask SetNormalActivity()
|
||||||
{
|
{
|
||||||
_ = await DiscordController.SetDefaultActivityAsync(runtimeOptions.AppLaunchTime).ConfigureAwait(false);
|
_ = await DiscordController.SetDefaultActivityAsync(runtimeOptions.AppLaunchTime).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Snap.Hutao.Service.Discord;
|
|||||||
|
|
||||||
internal interface IDiscordService
|
internal interface IDiscordService
|
||||||
{
|
{
|
||||||
ValueTask SetNormalActivityAsync();
|
ValueTask SetNormalActivity();
|
||||||
|
|
||||||
ValueTask SetPlayingActivityAsync(bool isOversea);
|
ValueTask SetPlayingActivity(bool isOversea);
|
||||||
}
|
}
|
||||||
@@ -226,7 +226,7 @@ internal sealed partial class GachaLogService : IGachaLogService
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Delay.RandomMilliSeconds(1000, 2000).ConfigureAwait(false);
|
await Delay.Random(1000, 2000).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
while (true);
|
while (true);
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ internal sealed partial class GachaLogService : IGachaLogService
|
|||||||
// save items for each queryType
|
// save items for each queryType
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
fetchContext.SaveItems();
|
fetchContext.SaveItems();
|
||||||
await Delay.RandomMilliSeconds(1000, 2000).ConfigureAwait(false);
|
await Delay.Random(1000, 2000).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new(!fetchContext.FetchStatus.AuthKeyTimeout, fetchContext.TargetArchive);
|
return new(!fetchContext.FetchStatus.AuthKeyTimeout, fetchContext.TargetArchive);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider;
|
|||||||
internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryProvider
|
internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryProvider
|
||||||
{
|
{
|
||||||
private readonly IContentDialogFactory contentDialogFactory;
|
private readonly IContentDialogFactory contentDialogFactory;
|
||||||
private readonly CultureOptions cultureOptions;
|
private readonly MetadataOptions metadataOptions;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async ValueTask<ValueResult<bool, GachaLogQuery>> GetQueryAsync()
|
public async ValueTask<ValueResult<bool, GachaLogQuery>> GetQueryAsync()
|
||||||
@@ -33,13 +33,13 @@ internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryP
|
|||||||
if (query.TryGetSingleValue("auth_appid", out string? appId) && appId is "webview_gacha")
|
if (query.TryGetSingleValue("auth_appid", out string? appId) && appId is "webview_gacha")
|
||||||
{
|
{
|
||||||
string? queryLanguageCode = query["lang"];
|
string? queryLanguageCode = query["lang"];
|
||||||
if (cultureOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
|
if (metadataOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
|
||||||
{
|
{
|
||||||
return new(true, new(queryString));
|
return new(true, new(queryString));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
string message = SH.FormatServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale(queryLanguageCode, cultureOptions.LanguageCode);
|
string message = SH.FormatServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale(queryLanguageCode, metadataOptions.LanguageCode);
|
||||||
return new(false, message);
|
return new(false, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider;
|
|||||||
internal sealed partial class GachaLogQuerySTokenProvider : IGachaLogQueryProvider
|
internal sealed partial class GachaLogQuerySTokenProvider : IGachaLogQueryProvider
|
||||||
{
|
{
|
||||||
private readonly BindingClient2 bindingClient2;
|
private readonly BindingClient2 bindingClient2;
|
||||||
private readonly CultureOptions cultureOptions;
|
private readonly MetadataOptions metadataOptions;
|
||||||
private readonly IUserService userService;
|
private readonly IUserService userService;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -38,7 +38,7 @@ internal sealed partial class GachaLogQuerySTokenProvider : IGachaLogQueryProvid
|
|||||||
|
|
||||||
if (authkeyResponse.IsOk())
|
if (authkeyResponse.IsOk())
|
||||||
{
|
{
|
||||||
return new(true, new(ComposeQueryString(data, authkeyResponse.Data, cultureOptions.LanguageCode)));
|
return new(true, new(ComposeQueryString(data, authkeyResponse.Data, metadataOptions.LanguageCode)));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider;
|
|||||||
internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProvider
|
internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProvider
|
||||||
{
|
{
|
||||||
private readonly IGameServiceFacade gameService;
|
private readonly IGameServiceFacade gameService;
|
||||||
private readonly CultureOptions cultureOptions;
|
private readonly MetadataOptions metadataOptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取缓存文件路径
|
/// 获取缓存文件路径
|
||||||
@@ -90,12 +90,12 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
|
|||||||
NameValueCollection query = HttpUtility.ParseQueryString(result.TrimEnd("#/log"));
|
NameValueCollection query = HttpUtility.ParseQueryString(result.TrimEnd("#/log"));
|
||||||
string? queryLanguageCode = query["lang"];
|
string? queryLanguageCode = query["lang"];
|
||||||
|
|
||||||
if (cultureOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
|
if (metadataOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
|
||||||
{
|
{
|
||||||
return new(true, new(result));
|
return new(true, new(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
string message = SH.FormatServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale(queryLanguageCode, cultureOptions.LanguageCode);
|
string message = SH.FormatServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale(queryLanguageCode, metadataOptions.LanguageCode);
|
||||||
return new(false, message);
|
return new(false, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ internal sealed partial class UIGFExportService : IUIGFExportService
|
|||||||
{
|
{
|
||||||
private readonly IGachaLogDbService gachaLogDbService;
|
private readonly IGachaLogDbService gachaLogDbService;
|
||||||
private readonly RuntimeOptions runtimeOptions;
|
private readonly RuntimeOptions runtimeOptions;
|
||||||
private readonly CultureOptions cultureOptions;
|
private readonly MetadataOptions metadataOptions;
|
||||||
private readonly ITaskContext taskContext;
|
private readonly ITaskContext taskContext;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -31,7 +31,7 @@ internal sealed partial class UIGFExportService : IUIGFExportService
|
|||||||
|
|
||||||
UIGF uigf = new()
|
UIGF uigf = new()
|
||||||
{
|
{
|
||||||
Info = UIGFInfo.From(runtimeOptions, cultureOptions, archive.Uid),
|
Info = UIGFInfo.From(runtimeOptions, metadataOptions, archive.Uid),
|
||||||
List = list,
|
List = list,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace Snap.Hutao.Service.GachaLog;
|
|||||||
internal sealed partial class UIGFImportService : IUIGFImportService
|
internal sealed partial class UIGFImportService : IUIGFImportService
|
||||||
{
|
{
|
||||||
private readonly ILogger<UIGFImportService> logger;
|
private readonly ILogger<UIGFImportService> logger;
|
||||||
private readonly CultureOptions cultureOptions;
|
private readonly MetadataOptions metadataOptions;
|
||||||
private readonly IGachaLogDbService gachaLogDbService;
|
private readonly IGachaLogDbService gachaLogDbService;
|
||||||
private readonly ITaskContext taskContext;
|
private readonly ITaskContext taskContext;
|
||||||
|
|
||||||
@@ -37,9 +37,9 @@ internal sealed partial class UIGFImportService : IUIGFImportService
|
|||||||
// v2.1 only support CHS
|
// v2.1 only support CHS
|
||||||
if (version is UIGFVersion.Major2Minor2OrLower)
|
if (version is UIGFVersion.Major2Minor2OrLower)
|
||||||
{
|
{
|
||||||
if (!cultureOptions.LanguageCodeFitsCurrentLocale(uigf.Info.Language))
|
if (!metadataOptions.LanguageCodeFitsCurrentLocale(uigf.Info.Language))
|
||||||
{
|
{
|
||||||
string message = SH.FormatServiceGachaUIGFImportLanguageNotMatch(uigf.Info.Language, cultureOptions.LanguageCode);
|
string message = SH.FormatServiceGachaUIGFImportLanguageNotMatch(uigf.Info.Language, metadataOptions.LanguageCode);
|
||||||
ThrowHelper.InvalidOperation(message);
|
ThrowHelper.InvalidOperation(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ internal readonly struct ChannelOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly bool IsOversea;
|
public readonly bool IsOversea;
|
||||||
|
|
||||||
public readonly ChannelOptionsErrorKind ErrorKind;
|
/// <summary>
|
||||||
|
/// 配置文件路径 当不为 null 时则存在文件读写问题
|
||||||
public readonly string? FilePath;
|
/// </summary>
|
||||||
|
public readonly string? ConfigFilePath;
|
||||||
|
|
||||||
public ChannelOptions(ChannelType channel, SubChannelType subChannel, bool isOversea)
|
public ChannelOptions(ChannelType channel, SubChannelType subChannel, bool isOversea)
|
||||||
{
|
{
|
||||||
@@ -47,20 +48,15 @@ internal readonly struct ChannelOptions
|
|||||||
IsOversea = isOversea;
|
IsOversea = isOversea;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChannelOptions(ChannelOptionsErrorKind errorKind, string? filePath)
|
private ChannelOptions(bool isOversea, string? configFilePath)
|
||||||
{
|
{
|
||||||
ErrorKind = errorKind;
|
IsOversea = isOversea;
|
||||||
FilePath = filePath;
|
ConfigFilePath = configFilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ChannelOptions ConfigurationFileNotFound(string filePath)
|
public static ChannelOptions FileNotFound(bool isOversea, string configFilePath)
|
||||||
{
|
{
|
||||||
return new(ChannelOptionsErrorKind.ConfigurationFileNotFound, filePath);
|
return new(isOversea, configFilePath);
|
||||||
}
|
|
||||||
|
|
||||||
public static ChannelOptions GamePathNullOrEmpty()
|
|
||||||
{
|
|
||||||
return new(ChannelOptionsErrorKind.GamePathNullOrEmpty, string.Empty);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Configuration;
|
|
||||||
|
|
||||||
internal enum ChannelOptionsErrorKind
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
ConfigurationFileNotFound,
|
|
||||||
GamePathNullOrEmpty,
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
using Snap.Hutao.Core.IO.Ini;
|
using Snap.Hutao.Core.IO.Ini;
|
||||||
using Snap.Hutao.Service.Game.Scheme;
|
using Snap.Hutao.Service.Game.Scheme;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using static Snap.Hutao.Service.Game.GameConstants;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Configuration;
|
namespace Snap.Hutao.Service.Game.Configuration;
|
||||||
|
|
||||||
@@ -15,22 +17,84 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
|
|||||||
|
|
||||||
public ChannelOptions GetChannelOptions()
|
public ChannelOptions GetChannelOptions()
|
||||||
{
|
{
|
||||||
if (!launchOptions.TryGetGameFileSystem(out GameFileSystem? gameFileSystem))
|
if (!launchOptions.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath))
|
||||||
{
|
{
|
||||||
return ChannelOptions.GamePathNullOrEmpty();
|
throw ThrowHelper.InvalidOperation($"Invalid game path: {gamePath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isOversea = LaunchScheme.ExecutableIsOversea(gameFileSystem.GameFileName);
|
bool isOversea = LaunchScheme.ExecutableIsOversea(Path.GetFileName(gamePath));
|
||||||
|
|
||||||
if (!File.Exists(gameFileSystem.GameConfigFilePath))
|
if (!File.Exists(configPath))
|
||||||
{
|
{
|
||||||
return ChannelOptions.ConfigurationFileNotFound(gameFileSystem.GameConfigFilePath);
|
return ChannelOptions.FileNotFound(isOversea, configPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<IniParameter> parameters = IniSerializer.DeserializeFromFile(gameFileSystem.GameConfigFilePath).OfType<IniParameter>().ToList();
|
using (FileStream stream = File.OpenRead(configPath))
|
||||||
string? channel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.ChannelName)?.Value;
|
{
|
||||||
string? subChannel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.SubChannelName)?.Value;
|
List<IniParameter> parameters = IniSerializer.Deserialize(stream).OfType<IniParameter>().ToList();
|
||||||
|
string? channel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.ChannelName)?.Value;
|
||||||
|
string? subChannel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.SubChannelName)?.Value;
|
||||||
|
|
||||||
return new(channel, subChannel, isOversea);
|
return new(channel, subChannel, isOversea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetChannelOptions(LaunchScheme scheme)
|
||||||
|
{
|
||||||
|
if (!launchOptions.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IniElement> elements = default!;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (FileStream readStream = File.OpenRead(configPath))
|
||||||
|
{
|
||||||
|
elements = [.. IniSerializer.Deserialize(readStream)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException ex)
|
||||||
|
{
|
||||||
|
ThrowHelper.GameFileOperation(SH.FormatServiceGameSetMultiChannelConfigFileNotFound(configPath), ex);
|
||||||
|
}
|
||||||
|
catch (DirectoryNotFoundException ex)
|
||||||
|
{
|
||||||
|
ThrowHelper.GameFileOperation(SH.FormatServiceGameSetMultiChannelConfigFileNotFound(configPath), ex);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelUnauthorizedAccess, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
foreach (IniElement element in elements)
|
||||||
|
{
|
||||||
|
if (element is IniParameter parameter)
|
||||||
|
{
|
||||||
|
if (parameter.Key is ChannelOptions.ChannelName)
|
||||||
|
{
|
||||||
|
changed = parameter.Set(scheme.Channel.ToString("D")) || changed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameter.Key is ChannelOptions.SubChannelName)
|
||||||
|
{
|
||||||
|
changed = parameter.Set(scheme.SubChannel.ToString("D")) || changed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
using (FileStream writeStream = File.Create(configPath))
|
||||||
|
{
|
||||||
|
IniSerializer.Serialize(writeStream, elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Service.Game.Scheme;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Configuration;
|
namespace Snap.Hutao.Service.Game.Configuration;
|
||||||
|
|
||||||
internal interface IGameChannelOptionsService
|
internal interface IGameChannelOptionsService
|
||||||
{
|
{
|
||||||
ChannelOptions GetChannelOptions();
|
ChannelOptions GetChannelOptions();
|
||||||
|
|
||||||
|
bool SetChannelOptions(LaunchScheme scheme);
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,6 @@ namespace Snap.Hutao.Service.Game;
|
|||||||
internal static class GameConstants
|
internal static class GameConstants
|
||||||
{
|
{
|
||||||
public const string ConfigFileName = "config.ini";
|
public const string ConfigFileName = "config.ini";
|
||||||
public const string PCGameSDKFilePath = @"YuanShen_Data\Plugins\PCGameSDK.dll";
|
|
||||||
public const string YuanShenFileName = "YuanShen.exe";
|
public const string YuanShenFileName = "YuanShen.exe";
|
||||||
public const string YuanShenFileNameUpper = "YUANSHEN.EXE";
|
public const string YuanShenFileNameUpper = "YUANSHEN.EXE";
|
||||||
public const string GenshinImpactFileName = "GenshinImpact.exe";
|
public const string GenshinImpactFileName = "GenshinImpact.exe";
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game;
|
|
||||||
|
|
||||||
internal sealed class GameFileSystem
|
|
||||||
{
|
|
||||||
private readonly string gameFilePath;
|
|
||||||
|
|
||||||
private string? gameFileName;
|
|
||||||
private string? gameDirectory;
|
|
||||||
private string? gameConfigFilePath;
|
|
||||||
private string? pcGameSDKFilePath;
|
|
||||||
|
|
||||||
public GameFileSystem(string gameFilePath)
|
|
||||||
{
|
|
||||||
this.gameFilePath = gameFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GameFilePath { get => gameFilePath; }
|
|
||||||
|
|
||||||
public string GameFileName { get => gameFileName ??= Path.GetFileName(gameFilePath); }
|
|
||||||
|
|
||||||
public string GameDirectory
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
gameDirectory ??= Path.GetDirectoryName(gameFilePath);
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(gameDirectory);
|
|
||||||
return gameDirectory;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GameConfigFilePath { get => gameConfigFilePath ??= Path.Combine(GameDirectory, GameConstants.ConfigFileName); }
|
|
||||||
|
|
||||||
public string PCGameSDKFilePath { get => pcGameSDKFilePath ??= Path.Combine(GameDirectory, GameConstants.PCGameSDKFilePath); }
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,10 @@ using Snap.Hutao.Model.Entity;
|
|||||||
using Snap.Hutao.Model.Entity.Primitive;
|
using Snap.Hutao.Model.Entity.Primitive;
|
||||||
using Snap.Hutao.Service.Game.Account;
|
using Snap.Hutao.Service.Game.Account;
|
||||||
using Snap.Hutao.Service.Game.Configuration;
|
using Snap.Hutao.Service.Game.Configuration;
|
||||||
using Snap.Hutao.Service.Game.Launching.Handler;
|
using Snap.Hutao.Service.Game.Package;
|
||||||
using Snap.Hutao.Service.Game.PathAbstraction;
|
using Snap.Hutao.Service.Game.PathAbstraction;
|
||||||
|
using Snap.Hutao.Service.Game.Process;
|
||||||
|
using Snap.Hutao.Service.Game.Scheme;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game;
|
namespace Snap.Hutao.Service.Game;
|
||||||
@@ -21,6 +23,8 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
|
|||||||
{
|
{
|
||||||
private readonly IGameChannelOptionsService gameChannelOptionsService;
|
private readonly IGameChannelOptionsService gameChannelOptionsService;
|
||||||
private readonly IGameAccountService gameAccountService;
|
private readonly IGameAccountService gameAccountService;
|
||||||
|
private readonly IGameProcessService gameProcessService;
|
||||||
|
private readonly IGamePackageService gamePackageService;
|
||||||
private readonly IGamePathService gamePathService;
|
private readonly IGamePathService gamePathService;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -41,6 +45,12 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
|
|||||||
return gameChannelOptionsService.GetChannelOptions();
|
return gameChannelOptionsService.GetChannelOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool SetChannelOptions(LaunchScheme scheme)
|
||||||
|
{
|
||||||
|
return gameChannelOptionsService.SetChannelOptions(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme)
|
public ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme)
|
||||||
{
|
{
|
||||||
@@ -53,6 +63,12 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
|
|||||||
return gameAccountService.DetectCurrentGameAccount(scheme);
|
return gameAccountService.DetectCurrentGameAccount(scheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool SetGameAccount(GameAccount account)
|
||||||
|
{
|
||||||
|
return gameAccountService.SetGameAccount(account);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
|
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
|
||||||
{
|
{
|
||||||
@@ -74,6 +90,18 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool IsGameRunning()
|
public bool IsGameRunning()
|
||||||
{
|
{
|
||||||
return LaunchExecutionEnsureGameNotRunningHandler.IsGameRunning(out _);
|
return gameProcessService.IsGameRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ValueTask LaunchAsync(IProgress<LaunchStatus> progress)
|
||||||
|
{
|
||||||
|
return gameProcessService.LaunchAsync(progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
|
||||||
|
{
|
||||||
|
return gamePackageService.EnsureGameResourceAsync(launchScheme, progress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
using Snap.Hutao.Model.Entity;
|
using Snap.Hutao.Model.Entity;
|
||||||
using Snap.Hutao.Model.Entity.Primitive;
|
using Snap.Hutao.Model.Entity.Primitive;
|
||||||
using Snap.Hutao.Service.Game.Configuration;
|
using Snap.Hutao.Service.Game.Configuration;
|
||||||
|
using Snap.Hutao.Service.Game.Package;
|
||||||
|
using Snap.Hutao.Service.Game.Scheme;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game;
|
namespace Snap.Hutao.Service.Game;
|
||||||
@@ -47,6 +49,8 @@ internal interface IGameServiceFacade
|
|||||||
/// <returns>是否正在运行</returns>
|
/// <returns>是否正在运行</returns>
|
||||||
bool IsGameRunning();
|
bool IsGameRunning();
|
||||||
|
|
||||||
|
ValueTask LaunchAsync(IProgress<LaunchStatus> progress);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 异步修改游戏账号名称
|
/// 异步修改游戏账号名称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -61,5 +65,27 @@ internal interface IGameServiceFacade
|
|||||||
/// <returns>任务</returns>
|
/// <returns>任务</returns>
|
||||||
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
|
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换游戏资源
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="launchScheme">目标启动方案</param>
|
||||||
|
/// <param name="progress">进度</param>
|
||||||
|
/// <returns>是否替换成功</returns>
|
||||||
|
ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改注册表中的账号信息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="account">账号</param>
|
||||||
|
/// <returns>是否设置成功</returns>
|
||||||
|
bool SetGameAccount(GameAccount account);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置多通道值
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="scheme">方案</param>
|
||||||
|
/// <returns>是否更改了ini文件</returns>
|
||||||
|
bool SetChannelOptions(LaunchScheme scheme);
|
||||||
|
|
||||||
GameAccount? DetectCurrentGameAccount(SchemeType scheme);
|
GameAccount? DetectCurrentGameAccount(SchemeType scheme);
|
||||||
}
|
}
|
||||||
@@ -218,7 +218,8 @@ internal sealed class LaunchOptions : DbStoreOptions
|
|||||||
{
|
{
|
||||||
if (SetProperty(ref selectedAspectRatio, value) && value is AspectRatio aspectRatio)
|
if (SetProperty(ref selectedAspectRatio, value) && value is AspectRatio aspectRatio)
|
||||||
{
|
{
|
||||||
(ScreenWidth, ScreenHeight) = ((int)aspectRatio.Width, (int)aspectRatio.Height);
|
ScreenWidth = (int)aspectRatio.Width;
|
||||||
|
ScreenHeight = (int)aspectRatio.Height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,25 +3,70 @@
|
|||||||
|
|
||||||
using Snap.Hutao.Service.Game.PathAbstraction;
|
using Snap.Hutao.Service.Game.PathAbstraction;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game;
|
namespace Snap.Hutao.Service.Game;
|
||||||
|
|
||||||
internal static class LaunchOptionsExtension
|
internal static class LaunchOptionsExtension
|
||||||
{
|
{
|
||||||
public static bool TryGetGameFileSystem(this LaunchOptions options, [NotNullWhen(true)] out GameFileSystem? fileSystem)
|
public static bool TryGetGamePathAndGameDirectory(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameDirectory)
|
||||||
{
|
{
|
||||||
string gamePath = options.GamePath;
|
gamePath = options.GamePath;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(gamePath))
|
gameDirectory = Path.GetDirectoryName(gamePath);
|
||||||
|
if (string.IsNullOrEmpty(gameDirectory))
|
||||||
{
|
{
|
||||||
fileSystem = default;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fileSystem = new GameFileSystem(gamePath);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryGetGameDirectoryAndGameFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameDirectory, [NotNullWhen(true)] out string? gameFileName)
|
||||||
|
{
|
||||||
|
string gamePath = options.GamePath;
|
||||||
|
|
||||||
|
gameDirectory = Path.GetDirectoryName(gamePath);
|
||||||
|
if (string.IsNullOrEmpty(gameDirectory))
|
||||||
|
{
|
||||||
|
gameFileName = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameFileName = Path.GetFileName(gamePath);
|
||||||
|
if (string.IsNullOrEmpty(gameFileName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetGamePathAndGameFileName(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameFileName)
|
||||||
|
{
|
||||||
|
gamePath = options.GamePath;
|
||||||
|
|
||||||
|
gameFileName = Path.GetFileName(gamePath);
|
||||||
|
if (string.IsNullOrEmpty(gameFileName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetGamePathAndFilePathByName(this LaunchOptions options, string fileName, out string gamePath, [NotNullWhen(true)] out string? filePath)
|
||||||
|
{
|
||||||
|
if (options.TryGetGamePathAndGameDirectory(out gamePath, out string? gameDirectory))
|
||||||
|
{
|
||||||
|
filePath = Path.Combine(gameDirectory, fileName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public static ImmutableList<GamePathEntry> GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry)
|
public static ImmutableList<GamePathEntry> GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry)
|
||||||
{
|
{
|
||||||
string gamePath = options.GamePath;
|
string gamePath = options.GamePath;
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
|
||||||
|
|
||||||
internal sealed class LaunchExecutionEnsureGameNotRunningHandler : ILaunchExecutionDelegateHandler
|
|
||||||
{
|
|
||||||
public static bool IsGameRunning([NotNullWhen(true)] out System.Diagnostics.Process? runningProcess)
|
|
||||||
{
|
|
||||||
// GetProcesses once and manually loop is O(n)
|
|
||||||
foreach (ref readonly System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses().AsSpan())
|
|
||||||
{
|
|
||||||
if (string.Equals(process.ProcessName, GameConstants.YuanShenProcessName, StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
string.Equals(process.ProcessName, GameConstants.GenshinImpactProcessName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
runningProcess = process;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runningProcess = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
|
||||||
{
|
|
||||||
if (IsGameRunning(out System.Diagnostics.Process? process))
|
|
||||||
{
|
|
||||||
context.Logger.LogInformation("Game process detected, id: {Id}", process.Id);
|
|
||||||
|
|
||||||
context.Result.Kind = LaunchExecutionResultKind.GameProcessRunning;
|
|
||||||
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGameIsRunning;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Microsoft.Win32.SafeHandles;
|
|
||||||
using Snap.Hutao.Control.Extension;
|
|
||||||
using Snap.Hutao.Factory.ContentDialog;
|
|
||||||
using Snap.Hutao.Factory.Progress;
|
|
||||||
using Snap.Hutao.Model.Intrinsic;
|
|
||||||
using Snap.Hutao.Service.Game.Package;
|
|
||||||
using Snap.Hutao.View.Dialog;
|
|
||||||
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
|
|
||||||
using Snap.Hutao.Web.Response;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
|
||||||
|
|
||||||
internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutionDelegateHandler
|
|
||||||
{
|
|
||||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
|
||||||
{
|
|
||||||
if (!context.TryGetGameFileSystem(out GameFileSystem? gameFileSystem))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ShouldConvert(context, gameFileSystem))
|
|
||||||
{
|
|
||||||
IServiceProvider serviceProvider = context.ServiceProvider;
|
|
||||||
IContentDialogFactory contentDialogFactory = serviceProvider.GetRequiredService<IContentDialogFactory>();
|
|
||||||
IProgressFactory progressFactory = serviceProvider.GetRequiredService<IProgressFactory>();
|
|
||||||
|
|
||||||
LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGamePackageConvertDialog>().ConfigureAwait(false);
|
|
||||||
IProgress<PackageConvertStatus> convertProgress = progressFactory.CreateForMainThread<PackageConvertStatus>(state => dialog.State = state);
|
|
||||||
|
|
||||||
using (await dialog.BlockAsync(context.TaskContext).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
if (!await EnsureGameResourceAsync(context, gameFileSystem, convertProgress).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
// context.Result is set in EnsureGameResourceAsync
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.TaskContext.SwitchToMainThreadAsync();
|
|
||||||
context.UpdateGamePathEntry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ShouldConvert(LaunchExecutionContext context, GameFileSystem gameFileSystem)
|
|
||||||
{
|
|
||||||
// Configuration file changed
|
|
||||||
if (context.ChannelOptionsChanged)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executable name not match
|
|
||||||
if (!context.Scheme.ExecutableMatches(gameFileSystem.GameFileName))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!context.Scheme.IsOversea)
|
|
||||||
{
|
|
||||||
// [It's Bilibili channel xor PCGameSDK.dll exists] means we need to convert
|
|
||||||
if (context.Scheme.Channel is ChannelType.Bili ^ File.Exists(gameFileSystem.PCGameSDKFilePath))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async ValueTask<bool> EnsureGameResourceAsync(LaunchExecutionContext context, GameFileSystem gameFileSystem, IProgress<PackageConvertStatus> progress)
|
|
||||||
{
|
|
||||||
string gameFolder = gameFileSystem.GameDirectory;
|
|
||||||
string gameFileName = gameFileSystem.GameFileName;
|
|
||||||
|
|
||||||
context.Logger.LogInformation("Game folder: {GameFolder}", gameFolder);
|
|
||||||
|
|
||||||
if (!CheckDirectoryPermissions(gameFolder))
|
|
||||||
{
|
|
||||||
context.Result.Kind = LaunchExecutionResultKind.GameDirectoryInsufficientPermissions;
|
|
||||||
context.Result.ErrorMessage = SH.ServiceGameEnsureGameResourceInsufficientDirectoryPermissions;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation));
|
|
||||||
|
|
||||||
ResourceClient resourceClient = context.ServiceProvider.GetRequiredService<ResourceClient>();
|
|
||||||
Response<GameResource> response = await resourceClient.GetResourceAsync(context.Scheme).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!response.TryGetDataWithoutUINotification(out GameResource? resource))
|
|
||||||
{
|
|
||||||
context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse;
|
|
||||||
context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(response);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
PackageConverter packageConverter = context.ServiceProvider.GetRequiredService<PackageConverter>();
|
|
||||||
|
|
||||||
if (!context.Scheme.ExecutableMatches(gameFileName))
|
|
||||||
{
|
|
||||||
if (!await packageConverter.EnsureGameResourceAsync(context.Scheme, resource, gameFolder, progress).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
context.Result.Kind = LaunchExecutionResultKind.GameResourcePackageConvertInternalError;
|
|
||||||
context.Result.ErrorMessage = SH.ViewModelLaunchGameEnsureGameResourceFail;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to change the gamePath if we switched.
|
|
||||||
string executableName = context.Scheme.IsOversea ? GameConstants.GenshinImpactFileName : GameConstants.YuanShenFileName;
|
|
||||||
|
|
||||||
await context.TaskContext.SwitchToMainThreadAsync();
|
|
||||||
context.Options.UpdateGamePathAndRefreshEntries(Path.Combine(gameFolder, executableName));
|
|
||||||
}
|
|
||||||
|
|
||||||
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
string tempFilePathMove = Path.Combine(folder, $"{Guid.NewGuid():N}.tmp");
|
|
||||||
|
|
||||||
// Test create file
|
|
||||||
using (SafeFileHandle handle = File.OpenHandle(tempFilePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, preallocationSize: 32 * 1024))
|
|
||||||
{
|
|
||||||
// Test write file
|
|
||||||
RandomAccess.Write(handle, "SNAP HUTAO DIRECTORY PERMISSION CHECK"u8, 0);
|
|
||||||
RandomAccess.FlushToDisk(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test move file
|
|
||||||
File.Move(tempFilePath, tempFilePathMove);
|
|
||||||
|
|
||||||
// Test delete file
|
|
||||||
File.Delete(tempFilePathMove);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
|
||||||
|
|
||||||
internal sealed class LaunchExecutionEnsureSchemeHandler : ILaunchExecutionDelegateHandler
|
|
||||||
{
|
|
||||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
|
||||||
{
|
|
||||||
if (context.Scheme is null)
|
|
||||||
{
|
|
||||||
context.Result.Kind = LaunchExecutionResultKind.NoActiveScheme;
|
|
||||||
context.Result.ErrorMessage = SH.ViewModelLaunchGameSchemeNotSelected;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Logger.LogInformation("Scheme [{Scheme}] is selected", context.Scheme.DisplayName);
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
|
||||||
|
|
||||||
internal sealed class LaunchExecutionGameProcessExitHandler : ILaunchExecutionDelegateHandler
|
|
||||||
{
|
|
||||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
|
||||||
{
|
|
||||||
if (!context.Process.HasExited)
|
|
||||||
{
|
|
||||||
context.Progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit));
|
|
||||||
await context.Process.WaitForExitAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Logger.LogInformation("Game process exited with code {ExitCode}", context.Process.ExitCode);
|
|
||||||
context.Progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited));
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Core;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
|
||||||
|
|
||||||
internal sealed class LaunchExecutionGameProcessInitializationHandler : ILaunchExecutionDelegateHandler
|
|
||||||
{
|
|
||||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
|
||||||
{
|
|
||||||
if (!context.TryGetGameFileSystem(out GameFileSystem? gameFileSystem))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing));
|
|
||||||
using (context.Process = InitializeGameProcess(context, gameFileSystem))
|
|
||||||
{
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static System.Diagnostics.Process InitializeGameProcess(LaunchExecutionContext context, GameFileSystem gameFileSystem)
|
|
||||||
{
|
|
||||||
LaunchOptions launchOptions = context.Options;
|
|
||||||
|
|
||||||
string commandLine = string.Empty;
|
|
||||||
if (launchOptions.IsEnabled)
|
|
||||||
{
|
|
||||||
// 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")
|
|
||||||
.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")
|
|
||||||
.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Logger.LogInformation("Command Line Arguments: {commandLine}", commandLine);
|
|
||||||
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
StartInfo = new()
|
|
||||||
{
|
|
||||||
Arguments = commandLine,
|
|
||||||
FileName = gameFileSystem.GameFilePath,
|
|
||||||
UseShellExecute = true,
|
|
||||||
Verb = "runas",
|
|
||||||
WorkingDirectory = gameFileSystem.GameDirectory,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Windows.Win32.Foundation;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
|
||||||
|
|
||||||
internal sealed class LaunchExecutionGameProcessStartHandler : ILaunchExecutionDelegateHandler
|
|
||||||
{
|
|
||||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context.Process.Start();
|
|
||||||
context.Logger.LogInformation("Process started");
|
|
||||||
}
|
|
||||||
catch (Win32Exception ex) when (ex.HResult == HRESULT.E_FAIL)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted));
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Core.IO.Ini;
|
|
||||||
using Snap.Hutao.Service.Game.Configuration;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
|
||||||
|
|
||||||
internal sealed class LaunchExecutionSetChannelOptionsHandler : ILaunchExecutionDelegateHandler
|
|
||||||
{
|
|
||||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
|
||||||
{
|
|
||||||
if (!context.TryGetGameFileSystem(out GameFileSystem? gameFileSystem))
|
|
||||||
{
|
|
||||||
// context.Result is set in TryGetGameFileSystem
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string configPath = gameFileSystem.GameConfigFilePath;
|
|
||||||
context.Logger.LogInformation("Game config file path: {ConfigPath}", configPath);
|
|
||||||
|
|
||||||
List<IniElement> elements = default!;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
elements = [.. IniSerializer.DeserializeFromFile(configPath)];
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException)
|
|
||||||
{
|
|
||||||
context.Result.Kind = LaunchExecutionResultKind.GameConfigFileNotFound;
|
|
||||||
context.Result.ErrorMessage = SH.FormatServiceGameSetMultiChannelConfigFileNotFound(configPath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (DirectoryNotFoundException)
|
|
||||||
{
|
|
||||||
context.Result.Kind = LaunchExecutionResultKind.GameConfigDirectoryNotFound;
|
|
||||||
context.Result.ErrorMessage = SH.FormatServiceGameSetMultiChannelConfigFileNotFound(configPath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
context.Result.Kind = LaunchExecutionResultKind.GameConfigInsufficientPermissions;
|
|
||||||
context.Result.ErrorMessage = SH.ServiceGameSetMultiChannelUnauthorizedAccess;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (IniElement element in elements)
|
|
||||||
{
|
|
||||||
if (element is IniParameter parameter)
|
|
||||||
{
|
|
||||||
if (parameter.Key is ChannelOptions.ChannelName)
|
|
||||||
{
|
|
||||||
context.ChannelOptionsChanged = parameter.Set(context.Scheme.Channel.ToString("D")) || context.ChannelOptionsChanged;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameter.Key is ChannelOptions.SubChannelName)
|
|
||||||
{
|
|
||||||
context.ChannelOptionsChanged = parameter.Set(context.Scheme.SubChannel.ToString("D")) || context.ChannelOptionsChanged;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context.ChannelOptionsChanged)
|
|
||||||
{
|
|
||||||
IniSerializer.SerializeToFile(configPath, elements);
|
|
||||||
}
|
|
||||||
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Service.Discord;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
|
||||||
|
|
||||||
internal sealed class LaunchExecutionSetDiscordActivityHandler : ILaunchExecutionDelegateHandler
|
|
||||||
{
|
|
||||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
|
||||||
{
|
|
||||||
bool previousSetDiscordActivityWhenPlaying = context.Options.SetDiscordActivityWhenPlaying;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (previousSetDiscordActivityWhenPlaying)
|
|
||||||
{
|
|
||||||
context.Logger.LogInformation("Set discord activity as playing");
|
|
||||||
await context.ServiceProvider
|
|
||||||
.GetRequiredService<IDiscordService>()
|
|
||||||
.SetPlayingActivityAsync(context.Scheme.IsOversea)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (previousSetDiscordActivityWhenPlaying)
|
|
||||||
{
|
|
||||||
context.Logger.LogInformation("Recover discord activity");
|
|
||||||
await context.ServiceProvider
|
|
||||||
.GetRequiredService<IDiscordService>()
|
|
||||||
.SetNormalActivityAsync()
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Service.Game.Account;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
|
||||||
|
|
||||||
internal sealed class LaunchExecutionSetGameAccountHandler : ILaunchExecutionDelegateHandler
|
|
||||||
{
|
|
||||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
|
||||||
{
|
|
||||||
if (context.Account is not null)
|
|
||||||
{
|
|
||||||
context.Logger.LogInformation("Set game account to [{Account}]", context.Account.Name);
|
|
||||||
|
|
||||||
if (!RegistryInterop.Set(context.Account))
|
|
||||||
{
|
|
||||||
context.Result.Kind = LaunchExecutionResultKind.GameAccountRegistryWriteResultNotMatch;
|
|
||||||
context.Result.ErrorMessage = SH.ViewModelLaunchGameSwitchGameAccountFail;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Service.Game.Account;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
|
||||||
|
|
||||||
internal sealed class LaunchExecutionSetWindowsHDRHandler : ILaunchExecutionDelegateHandler
|
|
||||||
{
|
|
||||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
|
||||||
{
|
|
||||||
if (context.Options.IsWindowsHDREnabled)
|
|
||||||
{
|
|
||||||
context.Logger.LogInformation("Set Windows HDR");
|
|
||||||
RegistryInterop.SetWindowsHDR(context.Scheme.IsOversea);
|
|
||||||
}
|
|
||||||
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Windows.System;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
|
||||||
|
|
||||||
internal sealed class LaunchExecutionStarwardPlayTimeStatisticsHandler : ILaunchExecutionDelegateHandler
|
|
||||||
{
|
|
||||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
|
||||||
{
|
|
||||||
if (context.Options.UseStarwardPlayTimeStatistics)
|
|
||||||
{
|
|
||||||
context.Logger.LogInformation("Using starward to count game time");
|
|
||||||
await LaunchStarwardForPlayTimeStatisticsAsync(context).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async ValueTask LaunchStarwardForPlayTimeStatisticsAsync(LaunchExecutionContext context)
|
|
||||||
{
|
|
||||||
string gameBiz = context.Scheme.IsOversea ? "hk4e_global" : "hk4e_cn";
|
|
||||||
Uri starwardPlayTimeUri = $"starward://playtime/{gameBiz}".ToUri();
|
|
||||||
if (await Launcher.QueryUriSupportAsync(starwardPlayTimeUri, LaunchQuerySupportType.Uri) is LaunchQuerySupportStatus.Available)
|
|
||||||
{
|
|
||||||
context.Logger.LogInformation("Launching starward");
|
|
||||||
await Launcher.LaunchUriAsync(starwardPlayTimeUri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Factory.Progress;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
|
||||||
|
|
||||||
internal sealed class LaunchExecutionStatusProgressHandler : ILaunchExecutionDelegateHandler
|
|
||||||
{
|
|
||||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
|
||||||
{
|
|
||||||
IProgressFactory progressFactory = context.ServiceProvider.GetRequiredService<IProgressFactory>();
|
|
||||||
LaunchStatusOptions statusOptions = context.ServiceProvider.GetRequiredService<LaunchStatusOptions>();
|
|
||||||
context.Progress = progressFactory.CreateForMainThread<LaunchStatus>(status => statusOptions.LaunchStatus = status);
|
|
||||||
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Clear status
|
|
||||||
context.Progress.Report(default!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Core;
|
|
||||||
using Snap.Hutao.Factory.Progress;
|
|
||||||
using Snap.Hutao.Service.Game.Unlocker;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
|
||||||
|
|
||||||
internal sealed class LaunchExecutionUnlockFpsHandler : ILaunchExecutionDelegateHandler
|
|
||||||
{
|
|
||||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
|
||||||
{
|
|
||||||
RuntimeOptions runtimeOptions = context.ServiceProvider.GetRequiredService<RuntimeOptions>();
|
|
||||||
if (runtimeOptions.IsElevated && context.Options.IsAdvancedLaunchOptionsEnabled && context.Options.UnlockFps)
|
|
||||||
{
|
|
||||||
context.Logger.LogInformation("Unlocking FPS");
|
|
||||||
context.Progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps));
|
|
||||||
|
|
||||||
IProgressFactory progressFactory = context.ServiceProvider.GetRequiredService<IProgressFactory>();
|
|
||||||
IProgress<UnlockerStatus> progress = progressFactory.CreateForMainThread<UnlockerStatus>(status => context.Progress.Report(LaunchStatus.FromUnlockStatus(status)));
|
|
||||||
GameFpsUnlocker unlocker = context.ServiceProvider.CreateInstance<GameFpsUnlocker>(context.Process);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await unlocker.UnlockAsync(new(100, 20000, 3000), progress, context.CancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex)
|
|
||||||
{
|
|
||||||
context.Logger.LogCritical(ex, "Unlocking FPS failed");
|
|
||||||
|
|
||||||
context.Result.Kind = LaunchExecutionResultKind.GameFpsUnlockingFailed;
|
|
||||||
context.Result.ErrorMessage = ex.Message;
|
|
||||||
|
|
||||||
// The Unlocker can't unlock the process
|
|
||||||
context.Process.Kill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await next().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching;
|
|
||||||
|
|
||||||
internal delegate ValueTask<LaunchExecutionContext> LaunchExecutionDelegate();
|
|
||||||
|
|
||||||
internal interface ILaunchExecutionDelegateHandler
|
|
||||||
{
|
|
||||||
ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next);
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Model.Entity;
|
|
||||||
using Snap.Hutao.Service.Game.PathAbstraction;
|
|
||||||
using Snap.Hutao.Service.Game.Scheme;
|
|
||||||
using Snap.Hutao.ViewModel.Game;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching;
|
|
||||||
|
|
||||||
[ConstructorGenerated]
|
|
||||||
internal sealed partial class LaunchExecutionContext
|
|
||||||
{
|
|
||||||
private readonly ILogger<LaunchExecutionContext> logger;
|
|
||||||
private readonly IServiceProvider serviceProvider;
|
|
||||||
private readonly ITaskContext taskContext;
|
|
||||||
private readonly LaunchOptions options;
|
|
||||||
|
|
||||||
private GameFileSystem? gameFileSystem;
|
|
||||||
|
|
||||||
[SuppressMessage("", "SH007")]
|
|
||||||
public LaunchExecutionContext(IServiceProvider serviceProvider, IViewModelSupportLaunchExecution viewModel, LaunchScheme? scheme, GameAccount? account)
|
|
||||||
: this(serviceProvider)
|
|
||||||
{
|
|
||||||
ViewModel = viewModel;
|
|
||||||
Scheme = scheme!;
|
|
||||||
Account = account;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LaunchExecutionResult Result { get; } = new();
|
|
||||||
|
|
||||||
public CancellationToken CancellationToken { get; set; }
|
|
||||||
|
|
||||||
public IServiceProvider ServiceProvider { get => serviceProvider; }
|
|
||||||
|
|
||||||
public ITaskContext TaskContext { get => taskContext; }
|
|
||||||
|
|
||||||
public ILogger Logger { get => logger; }
|
|
||||||
|
|
||||||
public LaunchOptions Options { get => options; }
|
|
||||||
|
|
||||||
public IViewModelSupportLaunchExecution ViewModel { get; private set; } = default!;
|
|
||||||
|
|
||||||
public LaunchScheme Scheme { get; private set; } = default!;
|
|
||||||
|
|
||||||
public GameAccount? Account { get; private set; }
|
|
||||||
|
|
||||||
public bool ChannelOptionsChanged { get; set; }
|
|
||||||
|
|
||||||
public IProgress<LaunchStatus> Progress { get; set; } = default!;
|
|
||||||
|
|
||||||
public System.Diagnostics.Process Process { get; set; } = default!;
|
|
||||||
|
|
||||||
public bool TryGetGameFileSystem([NotNullWhen(true)] out GameFileSystem? gameFileSystem)
|
|
||||||
{
|
|
||||||
if (this.gameFileSystem is not null)
|
|
||||||
{
|
|
||||||
gameFileSystem = this.gameFileSystem;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Options.TryGetGameFileSystem(out gameFileSystem))
|
|
||||||
{
|
|
||||||
Result.Kind = LaunchExecutionResultKind.NoActiveGamePath;
|
|
||||||
Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.gameFileSystem = gameFileSystem;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateGamePathEntry()
|
|
||||||
{
|
|
||||||
ImmutableList<GamePathEntry> gamePathEntries = Options.GetGamePathEntries(out GamePathEntry? selectedEntry);
|
|
||||||
ViewModel.SetGamePathEntriesAndSelectedGamePathEntry(gamePathEntries, selectedEntry);
|
|
||||||
|
|
||||||
// invalidate game file system
|
|
||||||
gameFileSystem = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Core;
|
|
||||||
using Snap.Hutao.Service.Game.Launching.Handler;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching;
|
|
||||||
|
|
||||||
[Injection(InjectAs.Transient)]
|
|
||||||
internal sealed class LaunchExecutionInvoker
|
|
||||||
{
|
|
||||||
private readonly Queue<ILaunchExecutionDelegateHandler> handlers;
|
|
||||||
|
|
||||||
public LaunchExecutionInvoker()
|
|
||||||
{
|
|
||||||
handlers = [];
|
|
||||||
handlers.Enqueue(new LaunchExecutionEnsureGameNotRunningHandler());
|
|
||||||
handlers.Enqueue(new LaunchExecutionEnsureSchemeHandler());
|
|
||||||
handlers.Enqueue(new LaunchExecutionSetChannelOptionsHandler());
|
|
||||||
handlers.Enqueue(new LaunchExecutionEnsureGameResourceHandler());
|
|
||||||
handlers.Enqueue(new LaunchExecutionSetGameAccountHandler());
|
|
||||||
handlers.Enqueue(new LaunchExecutionSetWindowsHDRHandler());
|
|
||||||
handlers.Enqueue(new LaunchExecutionStatusProgressHandler());
|
|
||||||
handlers.Enqueue(new LaunchExecutionGameProcessInitializationHandler());
|
|
||||||
handlers.Enqueue(new LaunchExecutionSetDiscordActivityHandler());
|
|
||||||
handlers.Enqueue(new LaunchExecutionGameProcessStartHandler());
|
|
||||||
handlers.Enqueue(new LaunchExecutionStarwardPlayTimeStatisticsHandler());
|
|
||||||
handlers.Enqueue(new LaunchExecutionUnlockFpsHandler());
|
|
||||||
handlers.Enqueue(new LaunchExecutionGameProcessExitHandler());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask<LaunchExecutionResult> InvokeAsync(LaunchExecutionContext context)
|
|
||||||
{
|
|
||||||
await InvokeHandlerAsync(context).ConfigureAwait(false);
|
|
||||||
return context.Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask<LaunchExecutionContext> InvokeHandlerAsync(LaunchExecutionContext context)
|
|
||||||
{
|
|
||||||
if (handlers.TryDequeue(out ILaunchExecutionDelegateHandler? handler))
|
|
||||||
{
|
|
||||||
string typeName = TypeNameHelper.GetTypeDisplayName(handler, false);
|
|
||||||
context.Logger.LogInformation("Handler[{Handler}] begin execution", typeName);
|
|
||||||
await handler.OnExecutionAsync(context, () => InvokeHandlerAsync(context)).ConfigureAwait(false);
|
|
||||||
context.Logger.LogInformation("Handler[{Handler}] end execution", typeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching;
|
|
||||||
|
|
||||||
internal sealed class LaunchExecutionResult
|
|
||||||
{
|
|
||||||
public LaunchExecutionResultKind Kind { get; set; }
|
|
||||||
|
|
||||||
public string ErrorMessage { get; set; } = default!;
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching;
|
|
||||||
|
|
||||||
internal enum LaunchExecutionResultKind
|
|
||||||
{
|
|
||||||
Ok,
|
|
||||||
NoActiveScheme,
|
|
||||||
NoActiveGamePath,
|
|
||||||
GameProcessRunning,
|
|
||||||
GameConfigFileNotFound,
|
|
||||||
GameConfigDirectoryNotFound,
|
|
||||||
GameConfigInsufficientPermissions,
|
|
||||||
GameDirectoryInsufficientPermissions,
|
|
||||||
GameResourceIndexQueryInvalidResponse,
|
|
||||||
GameResourcePackageConvertInternalError,
|
|
||||||
GameAccountRegistryWriteResultNotMatch,
|
|
||||||
GameFpsUnlockingFailed,
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Microsoft.Win32.SafeHandles;
|
||||||
|
using Snap.Hutao.Service.Game.Scheme;
|
||||||
|
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
|
||||||
|
using Snap.Hutao.Web.Response;
|
||||||
|
using System.IO;
|
||||||
|
using static Snap.Hutao.Service.Game.GameConstants;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Game.Package;
|
||||||
|
|
||||||
|
[ConstructorGenerated]
|
||||||
|
[Injection(InjectAs.Singleton, typeof(IGamePackageService))]
|
||||||
|
internal sealed partial class GamePackageService : IGamePackageService
|
||||||
|
{
|
||||||
|
private readonly PackageConverter packageConverter;
|
||||||
|
private readonly IServiceProvider serviceProvider;
|
||||||
|
private readonly LaunchOptions launchOptions;
|
||||||
|
private readonly ITaskContext taskContext;
|
||||||
|
|
||||||
|
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
|
||||||
|
{
|
||||||
|
if (!launchOptions.TryGetGameDirectoryAndGameFileName(out string? gameFolder, out string? gameFileName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CheckDirectoryPermissions(gameFolder))
|
||||||
|
{
|
||||||
|
progress.Report(new(SH.ServiceGameEnsureGameResourceInsufficientDirectoryPermissions));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation));
|
||||||
|
Response<GameResource> response = await serviceProvider
|
||||||
|
.GetRequiredService<ResourceClient>()
|
||||||
|
.GetResourceAsync(launchScheme)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!response.IsOk())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameResource resource = response.Data;
|
||||||
|
|
||||||
|
if (!launchScheme.ExecutableMatches(gameFileName))
|
||||||
|
{
|
||||||
|
// We can't start the game when we failed to convert game
|
||||||
|
if (!await packageConverter.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to change the gamePath if we switched.
|
||||||
|
string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName;
|
||||||
|
|
||||||
|
await taskContext.SwitchToMainThreadAsync();
|
||||||
|
launchOptions.UpdateGamePathAndRefreshEntries(Path.Combine(gameFolder, exeName));
|
||||||
|
}
|
||||||
|
|
||||||
|
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
string tempFilePathMove = Path.Combine(folder, $"{Guid.NewGuid():N}.tmp");
|
||||||
|
|
||||||
|
// Test create file
|
||||||
|
using (SafeFileHandle handle = File.OpenHandle(tempFilePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, preallocationSize: 32 * 1024))
|
||||||
|
{
|
||||||
|
// Test write file
|
||||||
|
RandomAccess.Write(handle, "SNAP HUTAO DIRECTORY PERMISSION CHECK"u8, 0);
|
||||||
|
RandomAccess.FlushToDisk(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test move file
|
||||||
|
File.Move(tempFilePath, tempFilePathMove);
|
||||||
|
|
||||||
|
// Test delete file
|
||||||
|
File.Delete(tempFilePathMove);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Service.Game.Scheme;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Game.Package;
|
||||||
|
|
||||||
|
internal interface IGamePackageService
|
||||||
|
{
|
||||||
|
ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ internal sealed partial class PackageConverter
|
|||||||
private readonly HttpClient httpClient;
|
private readonly HttpClient httpClient;
|
||||||
private readonly ILogger<PackageConverter> logger;
|
private readonly ILogger<PackageConverter> logger;
|
||||||
|
|
||||||
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResource, string gameFolder, IProgress<PackageConvertStatus> progress)
|
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResource, string gameFolder, IProgress<PackageReplaceStatus> progress)
|
||||||
{
|
{
|
||||||
// 以 国服 => 国际 为例
|
// 以 国服 => 国际 为例
|
||||||
// 1. 下载国际服的 pkg_version 文件,转换为索引字典
|
// 1. 下载国际服的 pkg_version 文件,转换为索引字典
|
||||||
@@ -93,6 +93,7 @@ internal sealed partial class PackageConverter
|
|||||||
ZipFile.ExtractToDirectory(sdkWebStream, gameFolder, true);
|
ZipFile.ExtractToDirectory(sdkWebStream, gameFolder, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: verify sdk md5
|
||||||
if (File.Exists(sdkDllBackup) && File.Exists(sdkVersionBackup))
|
if (File.Exists(sdkDllBackup) && File.Exists(sdkVersionBackup))
|
||||||
{
|
{
|
||||||
File.Delete(sdkDllBackup);
|
File.Delete(sdkDllBackup);
|
||||||
@@ -187,7 +188,7 @@ internal sealed partial class PackageConverter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask PrepareCacheFilesAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageConvertStatus> progress)
|
private async ValueTask PrepareCacheFilesAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
|
||||||
{
|
{
|
||||||
foreach (PackageItemOperationInfo info in operations)
|
foreach (PackageItemOperationInfo info in operations)
|
||||||
{
|
{
|
||||||
@@ -203,7 +204,7 @@ internal sealed partial class PackageConverter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress<PackageConvertStatus> progress)
|
private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
|
||||||
{
|
{
|
||||||
// 还原正确的远程地址
|
// 还原正确的远程地址
|
||||||
string remoteName = string.Format(CultureInfo.CurrentCulture, info.Remote.RelativePath, context.ToDataFolderName);
|
string remoteName = string.Format(CultureInfo.CurrentCulture, info.Remote.RelativePath, context.ToDataFolderName);
|
||||||
@@ -229,7 +230,7 @@ internal sealed partial class PackageConverter
|
|||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
|
|
||||||
string remoteUrl = context.GetScatteredFilesUrl(remoteName);
|
string remoteUrl = context.GetScatteredFilesUrl(remoteName);
|
||||||
HttpShardCopyWorkerOptions<PackageConvertStatus> options = new()
|
HttpShardCopyWorkerOptions<PackageReplaceStatus> options = new()
|
||||||
{
|
{
|
||||||
HttpClient = httpClient,
|
HttpClient = httpClient,
|
||||||
SourceUrl = remoteUrl,
|
SourceUrl = remoteUrl,
|
||||||
@@ -237,7 +238,7 @@ internal sealed partial class PackageConverter
|
|||||||
StatusFactory = (bytesRead, totalBytes) => new(remoteName, bytesRead, totalBytes),
|
StatusFactory = (bytesRead, totalBytes) => new(remoteName, bytesRead, totalBytes),
|
||||||
};
|
};
|
||||||
|
|
||||||
using (HttpShardCopyWorker<PackageConvertStatus> worker = await HttpShardCopyWorker<PackageConvertStatus>.CreateAsync(options).ConfigureAwait(false))
|
using (HttpShardCopyWorker<PackageReplaceStatus> worker = await HttpShardCopyWorker<PackageReplaceStatus>.CreateAsync(options).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -257,7 +258,7 @@ internal sealed partial class PackageConverter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<bool> ReplaceGameResourceAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageConvertStatus> progress)
|
private async ValueTask<bool> ReplaceGameResourceAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
|
||||||
{
|
{
|
||||||
// 执行下载与移动操作
|
// 执行下载与移动操作
|
||||||
foreach (PackageItemOperationInfo info in operations)
|
foreach (PackageItemOperationInfo info in operations)
|
||||||
|
|||||||
@@ -8,15 +8,25 @@ namespace Snap.Hutao.Service.Game.Package;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 包更新状态
|
/// 包更新状态
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class PackageConvertStatus
|
internal sealed class PackageReplaceStatus
|
||||||
{
|
{
|
||||||
public PackageConvertStatus(string name)
|
/// <summary>
|
||||||
|
/// 构造一个新的包更新状态
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">描述</param>
|
||||||
|
public PackageReplaceStatus(string name)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
Description = name;
|
Description = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PackageConvertStatus(string name, long bytesRead, long totalBytes)
|
/// <summary>
|
||||||
|
/// 构造一个新的包更新状态
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">名称</param>
|
||||||
|
/// <param name="bytesRead">读取的字节数</param>
|
||||||
|
/// <param name="totalBytes">总字节数</param>
|
||||||
|
public PackageReplaceStatus(string name, long bytesRead, long totalBytes)
|
||||||
{
|
{
|
||||||
Percent = (double)bytesRead / totalBytes;
|
Percent = (double)bytesRead / totalBytes;
|
||||||
Name = name;
|
Name = name;
|
||||||
@@ -9,7 +9,7 @@ internal sealed class GamePathEntry
|
|||||||
public string Path { get; set; } = default!;
|
public string Path { get; set; } = default!;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public GamePathEntryKind Kind { get => GetKind(Path); }
|
public GamePathKind Kind { get => GetKind(Path); }
|
||||||
|
|
||||||
public static GamePathEntry Create(string path)
|
public static GamePathEntry Create(string path)
|
||||||
{
|
{
|
||||||
@@ -19,8 +19,8 @@ internal sealed class GamePathEntry
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GamePathEntryKind GetKind(string path)
|
private static GamePathKind GetKind(string path)
|
||||||
{
|
{
|
||||||
return GamePathEntryKind.None;
|
return GamePathKind.None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.PathAbstraction;
|
namespace Snap.Hutao.Service.Game.PathAbstraction;
|
||||||
|
|
||||||
internal enum GamePathEntryKind
|
internal enum GamePathKind
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
ChineseClient,
|
ChineseClient,
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core;
|
||||||
|
using Snap.Hutao.Factory.Progress;
|
||||||
|
using Snap.Hutao.Service.Discord;
|
||||||
|
using Snap.Hutao.Service.Game.Account;
|
||||||
|
using Snap.Hutao.Service.Game.Scheme;
|
||||||
|
using Snap.Hutao.Service.Game.Unlocker;
|
||||||
|
using System.IO;
|
||||||
|
using static Snap.Hutao.Service.Game.GameConstants;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Game.Process;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程互操作
|
||||||
|
/// </summary>
|
||||||
|
[ConstructorGenerated]
|
||||||
|
[Injection(InjectAs.Singleton, typeof(IGameProcessService))]
|
||||||
|
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;
|
||||||
|
|
||||||
|
private volatile bool isGameRunning;
|
||||||
|
|
||||||
|
public bool IsGameRunning()
|
||||||
|
{
|
||||||
|
if (isGameRunning)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original two GetProcessesByName is O(2n)
|
||||||
|
// GetProcesses once and manually loop is O(n)
|
||||||
|
foreach (ref System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses().AsSpan())
|
||||||
|
{
|
||||||
|
if (process.ProcessName is YuanShenProcessName or GenshinImpactProcessName)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask LaunchAsync(IProgress<LaunchStatus> progress)
|
||||||
|
{
|
||||||
|
if (IsGameRunning())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!launchOptions.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName))
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrEmpty(gamePath);
|
||||||
|
return; // null check passing, actually never reach.
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isOversea = LaunchScheme.ExecutableIsOversea(gameFileName);
|
||||||
|
|
||||||
|
if (launchOptions.IsWindowsHDREnabled)
|
||||||
|
{
|
||||||
|
RegistryInterop.SetWindowsHDR(isOversea);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing));
|
||||||
|
using (System.Diagnostics.Process game = InitializeGameProcess(gamePath))
|
||||||
|
{
|
||||||
|
await using (await GameRunningTracker.CreateAsync(this, isOversea).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
game.Start();
|
||||||
|
progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted));
|
||||||
|
|
||||||
|
if (launchOptions.UseStarwardPlayTimeStatistics)
|
||||||
|
{
|
||||||
|
await Starward.LaunchForPlayTimeStatisticsAsync(isOversea).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runtimeOptions.IsElevated && launchOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps)
|
||||||
|
{
|
||||||
|
progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await UnlockFpsAsync(game, progress).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
// The Unlocker can't unlock the process
|
||||||
|
game.Kill();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit));
|
||||||
|
await game.WaitForExitAsync().ConfigureAwait(false);
|
||||||
|
progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private System.Diagnostics.Process InitializeGameProcess(string gamePath)
|
||||||
|
{
|
||||||
|
string commandLine = string.Empty;
|
||||||
|
|
||||||
|
if (launchOptions.IsEnabled)
|
||||||
|
{
|
||||||
|
// 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")
|
||||||
|
.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")
|
||||||
|
.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
StartInfo = new()
|
||||||
|
{
|
||||||
|
Arguments = commandLine,
|
||||||
|
FileName = gamePath,
|
||||||
|
UseShellExecute = true,
|
||||||
|
Verb = "runas",
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(gamePath),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueTask UnlockFpsAsync(System.Diagnostics.Process game, IProgress<LaunchStatus> progress, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
#pragma warning disable CA1859
|
||||||
|
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)));
|
||||||
|
return unlocker.UnlockAsync(options, lockerProgress, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GameRunningTracker : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly GameProcessService service;
|
||||||
|
private readonly bool previousSetDiscordActivityWhenPlaying;
|
||||||
|
|
||||||
|
private GameRunningTracker(GameProcessService service, bool isOversea)
|
||||||
|
{
|
||||||
|
service.isGameRunning = true;
|
||||||
|
previousSetDiscordActivityWhenPlaying = service.launchOptions.SetDiscordActivityWhenPlaying;
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async ValueTask<GameRunningTracker> CreateAsync(GameProcessService service, bool isOversea)
|
||||||
|
{
|
||||||
|
GameRunningTracker tracker = new(service, isOversea);
|
||||||
|
if (tracker.previousSetDiscordActivityWhenPlaying)
|
||||||
|
{
|
||||||
|
await service.discordService.SetPlayingActivity(isOversea).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (previousSetDiscordActivityWhenPlaying)
|
||||||
|
{
|
||||||
|
await service.discordService.SetNormalActivity().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
service.isGameRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Game.Process;
|
||||||
|
|
||||||
|
internal interface IGameProcessService
|
||||||
|
{
|
||||||
|
bool IsGameRunning();
|
||||||
|
|
||||||
|
ValueTask LaunchAsync(IProgress<LaunchStatus> progress);
|
||||||
|
}
|
||||||
19
src/Snap.Hutao/Snap.Hutao/Service/Game/Process/Starward.cs
Normal file
19
src/Snap.Hutao/Snap.Hutao/Service/Game/Process/Starward.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Windows.System;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Game.Process;
|
||||||
|
|
||||||
|
internal static class Starward
|
||||||
|
{
|
||||||
|
public static async ValueTask LaunchForPlayTimeStatisticsAsync(bool isOversea)
|
||||||
|
{
|
||||||
|
string gameBiz = isOversea ? "hk4e_global" : "hk4e_cn";
|
||||||
|
Uri starwardPlayTimeUri = $"starward://playtime/{gameBiz}".ToUri();
|
||||||
|
if (await Launcher.QueryUriSupportAsync(starwardPlayTimeUri, LaunchQuerySupportType.Uri) is LaunchQuerySupportStatus.Available)
|
||||||
|
{
|
||||||
|
await Launcher.LaunchUriAsync(starwardPlayTimeUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
namespace Snap.Hutao.Service;
|
namespace Snap.Hutao.Service.Metadata;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 本地化名称
|
/// 本地化名称
|
||||||
@@ -74,15 +74,4 @@ internal static class LocaleNames
|
|||||||
|
|
||||||
return !string.IsNullOrEmpty(languageCode);
|
return !string.IsNullOrEmpty(languageCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetLanguageCodeForDocumentationSearchFromLocaleName(string localeName)
|
|
||||||
{
|
|
||||||
return localeName switch
|
|
||||||
{
|
|
||||||
ID => "id-id",
|
|
||||||
RU => "ru-ru",
|
|
||||||
CHS => "zh-cn",
|
|
||||||
_ => "en-us",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Snap.Hutao.Core;
|
using Snap.Hutao.Core;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Metadata;
|
namespace Snap.Hutao.Service.Metadata;
|
||||||
@@ -11,9 +10,10 @@ namespace Snap.Hutao.Service.Metadata;
|
|||||||
[Injection(InjectAs.Singleton)]
|
[Injection(InjectAs.Singleton)]
|
||||||
internal sealed partial class MetadataOptions
|
internal sealed partial class MetadataOptions
|
||||||
{
|
{
|
||||||
private readonly CultureOptions cultureOptions;
|
private readonly AppOptions appOptions;
|
||||||
private readonly RuntimeOptions runtimeOptions;
|
private readonly RuntimeOptions hutaoOptions;
|
||||||
|
|
||||||
|
private string? localeName;
|
||||||
private string? fallbackDataFolder;
|
private string? fallbackDataFolder;
|
||||||
private string? localizedDataFolder;
|
private string? localizedDataFolder;
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ internal sealed partial class MetadataOptions
|
|||||||
{
|
{
|
||||||
if (fallbackDataFolder is null)
|
if (fallbackDataFolder is null)
|
||||||
{
|
{
|
||||||
fallbackDataFolder = Path.Combine(runtimeOptions.DataFolder, "Metadata", LocaleNames.CHS);
|
fallbackDataFolder = Path.Combine(hutaoOptions.DataFolder, "Metadata", "CHS");
|
||||||
Directory.CreateDirectory(fallbackDataFolder);
|
Directory.CreateDirectory(fallbackDataFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ internal sealed partial class MetadataOptions
|
|||||||
{
|
{
|
||||||
if (localizedDataFolder is null)
|
if (localizedDataFolder is null)
|
||||||
{
|
{
|
||||||
localizedDataFolder = Path.Combine(runtimeOptions.DataFolder, "Metadata", cultureOptions.LocaleName);
|
localizedDataFolder = Path.Combine(hutaoOptions.DataFolder, "Metadata", LocaleName);
|
||||||
Directory.CreateDirectory(localizedDataFolder);
|
Directory.CreateDirectory(localizedDataFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,13 +45,21 @@ internal sealed partial class MetadataOptions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetLocalizedLocalFile(string fileNameWithExtension)
|
public string LocaleName
|
||||||
{
|
{
|
||||||
return Path.Combine(LocalizedDataFolder, fileNameWithExtension);
|
get => localeName ??= MetadataOptionsExtension.GetLocaleName(appOptions.CurrentCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetLocalizedRemoteFile(string fileNameWithExtension)
|
public string LanguageCode
|
||||||
{
|
{
|
||||||
return Web.HutaoEndpoints.Metadata(cultureOptions.LocaleName, fileNameWithExtension);
|
get
|
||||||
|
{
|
||||||
|
if (LocaleNames.TryGetLanguageCodeFromLocaleName(LocaleName, out string? languageCode))
|
||||||
|
{
|
||||||
|
return languageCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new KeyNotFoundException($"Invalid localeName: '{LocaleName}'");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Snap.Hutao.Model;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service;
|
namespace Snap.Hutao.Service.Metadata;
|
||||||
|
|
||||||
internal static class CultureOptionsExtension
|
internal static class MetadataOptionsExtension
|
||||||
{
|
{
|
||||||
public static NameValue<CultureInfo>? GetCurrentCultureForSelectionOrDefault(this CultureOptions options)
|
public static string GetLocalizedLocalFile(this MetadataOptions options, string fileNameWithExtension)
|
||||||
{
|
{
|
||||||
return options.Cultures.SingleOrDefault(c => c.Value == options.CurrentCulture);
|
return Path.Combine(options.LocalizedDataFolder, fileNameWithExtension);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool LanguageCodeFitsCurrentLocale(this CultureOptions options, string? languageCode)
|
public static string GetLocalizedRemoteFile(this MetadataOptions options, string fileNameWithExtension)
|
||||||
|
{
|
||||||
|
return Web.HutaoEndpoints.Metadata(options.LocaleName, fileNameWithExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool LanguageCodeFitsCurrentLocale(this MetadataOptions options, string? languageCode)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(languageCode))
|
if (string.IsNullOrEmpty(languageCode))
|
||||||
{
|
{
|
||||||
@@ -25,11 +30,6 @@ internal static class CultureOptionsExtension
|
|||||||
return GetLocaleName(cultureInfo) == options.LocaleName;
|
return GetLocaleName(cultureInfo) == options.LocaleName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetLanguageCodeForDocumentationSearch(this CultureOptions options)
|
|
||||||
{
|
|
||||||
return LocaleNames.GetLanguageCodeForDocumentationSearchFromLocaleName(options.LocaleName);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static string GetLocaleName(CultureInfo cultureInfo)
|
internal static string GetLocaleName(CultureInfo cultureInfo)
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.View.Page;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Navigation;
|
||||||
|
|
||||||
|
[Injection(InjectAs.Singleton, typeof(IDocumentationProvider))]
|
||||||
|
[ConstructorGenerated]
|
||||||
|
internal sealed partial class DocumentationProvider : IDocumentationProvider
|
||||||
|
{
|
||||||
|
private const string Home = "https://hut.ao";
|
||||||
|
|
||||||
|
private static readonly Dictionary<Type, string> TypeDocumentations = new()
|
||||||
|
{
|
||||||
|
[typeof(AchievementPage)] = "https://hut.ao/features/achievements.html",
|
||||||
|
[typeof(AnnouncementPage)] = "https://hut.ao/features/dashboard.html",
|
||||||
|
[typeof(AvatarPropertyPage)] = "https://hut.ao/features/character-data.html",
|
||||||
|
[typeof(CultivationPage)] = "https://hut.ao/features/develop-plan.html",
|
||||||
|
[typeof(DailyNotePage)] = "https://hut.ao/features/real-time-notes.html",
|
||||||
|
[typeof(GachaLogPage)] = "https://hut.ao/features/wish-export.html",
|
||||||
|
[typeof(LaunchGamePage)] = "https://hut.ao/features/game-launcher.html",
|
||||||
|
[typeof(LoginHoyoverseUserPage)] = "https://hut.ao/features/mhy-account-switch.html",
|
||||||
|
[typeof(LoginMihoyoUserPage)] = "https://hut.ao/features/mhy-account-switch.html",
|
||||||
|
[typeof(SettingPage)] = "https://hut.ao/features/hutao-settings.html",
|
||||||
|
[typeof(SpiralAbyssRecordPage)] = "https://hut.ao/features/hutao-API.html",
|
||||||
|
[typeof(TestPage)] = Home,
|
||||||
|
[typeof(WikiAvatarPage)] = "https://hut.ao/features/character-wiki.html",
|
||||||
|
[typeof(WikiMonsterPage)] = "https://hut.ao/features/monster-wiki.html",
|
||||||
|
[typeof(WikiWeaponPage)] = "https://hut.ao/features/weapon-wiki.html",
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly INavigationService navigationService;
|
||||||
|
|
||||||
|
public string GetDocumentation()
|
||||||
|
{
|
||||||
|
if (navigationService.Current is { } type)
|
||||||
|
{
|
||||||
|
return TypeDocumentations[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Home;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Navigation;
|
||||||
|
|
||||||
|
internal interface IDocumentationProvider
|
||||||
|
{
|
||||||
|
string GetDocumentation();
|
||||||
|
}
|
||||||
@@ -107,7 +107,6 @@
|
|||||||
<None Remove="Control\Theme\Uri.xaml" />
|
<None Remove="Control\Theme\Uri.xaml" />
|
||||||
<None Remove="Control\Theme\WindowOverride.xaml" />
|
<None Remove="Control\Theme\WindowOverride.xaml" />
|
||||||
<None Remove="GuideWindow.xaml" />
|
<None Remove="GuideWindow.xaml" />
|
||||||
<None Remove="IdentifyMonitorWindow.xaml" />
|
|
||||||
<None Remove="IdentityStructs.json" />
|
<None Remove="IdentityStructs.json" />
|
||||||
<None Remove="LaunchGameWindow.xaml" />
|
<None Remove="LaunchGameWindow.xaml" />
|
||||||
<None Remove="Resource\BlurBackground.png" />
|
<None Remove="Resource\BlurBackground.png" />
|
||||||
@@ -128,7 +127,6 @@
|
|||||||
<None Remove="Resource\Navigation\DailyNote.png" />
|
<None Remove="Resource\Navigation\DailyNote.png" />
|
||||||
<None Remove="Resource\Navigation\Database.png" />
|
<None Remove="Resource\Navigation\Database.png" />
|
||||||
<None Remove="Resource\Navigation\Documentation.png" />
|
<None Remove="Resource\Navigation\Documentation.png" />
|
||||||
<None Remove="Resource\Navigation\Feedback.png" />
|
|
||||||
<None Remove="Resource\Navigation\GachaLog.png" />
|
<None Remove="Resource\Navigation\GachaLog.png" />
|
||||||
<None Remove="Resource\Navigation\LaunchGame.png" />
|
<None Remove="Resource\Navigation\LaunchGame.png" />
|
||||||
<None Remove="Resource\Navigation\SpiralAbyss.png" />
|
<None Remove="Resource\Navigation\SpiralAbyss.png" />
|
||||||
@@ -187,7 +185,6 @@
|
|||||||
<None Remove="View\Page\AvatarPropertyPage.xaml" />
|
<None Remove="View\Page\AvatarPropertyPage.xaml" />
|
||||||
<None Remove="View\Page\CultivationPage.xaml" />
|
<None Remove="View\Page\CultivationPage.xaml" />
|
||||||
<None Remove="View\Page\DailyNotePage.xaml" />
|
<None Remove="View\Page\DailyNotePage.xaml" />
|
||||||
<None Remove="View\Page\FeedbackPage.xaml" />
|
|
||||||
<None Remove="View\Page\GachaLogPage.xaml" />
|
<None Remove="View\Page\GachaLogPage.xaml" />
|
||||||
<None Remove="View\Page\LaunchGamePage.xaml" />
|
<None Remove="View\Page\LaunchGamePage.xaml" />
|
||||||
<None Remove="View\Page\LoginMihoyoUserPage.xaml" />
|
<None Remove="View\Page\LoginMihoyoUserPage.xaml" />
|
||||||
@@ -271,7 +268,6 @@
|
|||||||
<Content Include="Resource\Navigation\DailyNote.png" />
|
<Content Include="Resource\Navigation\DailyNote.png" />
|
||||||
<Content Include="Resource\Navigation\Database.png" />
|
<Content Include="Resource\Navigation\Database.png" />
|
||||||
<Content Include="Resource\Navigation\Documentation.png" />
|
<Content Include="Resource\Navigation\Documentation.png" />
|
||||||
<Content Include="Resource\Navigation\Feedback.png" />
|
|
||||||
<Content Include="Resource\Navigation\GachaLog.png" />
|
<Content Include="Resource\Navigation\GachaLog.png" />
|
||||||
<Content Include="Resource\Navigation\LaunchGame.png" />
|
<Content Include="Resource\Navigation\LaunchGame.png" />
|
||||||
<Content Include="Resource\Navigation\SpiralAbyss.png" />
|
<Content Include="Resource\Navigation\SpiralAbyss.png" />
|
||||||
@@ -325,7 +321,7 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.556">
|
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.507">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@@ -348,11 +344,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<Page Update="View\Page\FeedbackPage.xaml">
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Page Update="View\Dialog\ReconfirmDialog.xaml">
|
<Page Update="View\Dialog\ReconfirmDialog.xaml">
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
@@ -554,12 +545,6 @@
|
|||||||
</Page>
|
</Page>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Page Update="IdentifyMonitorWindow.xaml">
|
|
||||||
<Generator>MSBuild:Compile</Generator>
|
|
||||||
</Page>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Page Update="View\Control\HutaoStatisticsCard.xaml">
|
<Page Update="View\Control\HutaoStatisticsCard.xaml">
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<FontIcon
|
<FontIcon
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
Grid.Column="0"
|
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
FontSize="{ThemeResource TitleTextBlockFontSize}"
|
FontSize="{ThemeResource TitleTextBlockFontSize}"
|
||||||
@@ -62,22 +61,16 @@
|
|||||||
Content="{StaticResource FontIconContentSetting}"
|
Content="{StaticResource FontIconContentSetting}"
|
||||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageHomeLaunchGameSettingAction}"/>
|
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageHomeLaunchGameSettingAction}"/>
|
||||||
<StackPanel
|
<shc:SizeRestrictedContentControl
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
VerticalAlignment="Bottom"
|
Grid.ColumnSpan="3"
|
||||||
Spacing="8">
|
VerticalAlignment="Bottom">
|
||||||
<TextBlock
|
<ComboBox
|
||||||
Opacity="0.7"
|
DisplayMemberPath="Name"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
ItemsSource="{Binding GameAccountsView}"
|
||||||
Text="{Binding LaunchStatusOptions.LaunchStatus.Description, Mode=OneWay}"/>
|
PlaceholderText="{shcm:ResourceString Name=ViewCardLaunchGameSelectAccountPlaceholder}"
|
||||||
<shc:SizeRestrictedContentControl VerticalAlignment="Bottom">
|
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
|
||||||
<ComboBox
|
</shc:SizeRestrictedContentControl>
|
||||||
DisplayMemberPath="Name"
|
|
||||||
ItemsSource="{Binding GameAccountsView}"
|
|
||||||
PlaceholderText="{shcm:ResourceString Name=ViewCardLaunchGameSelectAccountPlaceholder}"
|
|
||||||
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
|
|
||||||
</shc:SizeRestrictedContentControl>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -11,7 +11,7 @@ namespace Snap.Hutao.View.Dialog;
|
|||||||
/// 启动游戏客户端转换对话框
|
/// 启动游戏客户端转换对话框
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HighQuality]
|
[HighQuality]
|
||||||
[DependencyProperty("State", typeof(PackageConvertStatus))]
|
[DependencyProperty("State", typeof(PackageReplaceStatus))]
|
||||||
internal sealed partial class LaunchGamePackageConvertDialog : ContentDialog
|
internal sealed partial class LaunchGamePackageConvertDialog : ContentDialog
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<GridView
|
<GridView
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
ItemTemplate="{StaticResource LanguageTemplate}"
|
ItemTemplate="{StaticResource LanguageTemplate}"
|
||||||
ItemsSource="{Binding CultureOptions.Cultures}"
|
ItemsSource="{Binding AppOptions.Cultures}"
|
||||||
SelectedItem="{Binding SelectedCulture, Mode=TwoWay}"
|
SelectedItem="{Binding SelectedCulture, Mode=TwoWay}"
|
||||||
SelectionMode="Single"/>
|
SelectionMode="Single"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -27,10 +27,6 @@
|
|||||||
shvh:NavHelper.NavigateTo="shvp:AnnouncementPage"
|
shvh:NavHelper.NavigateTo="shvp:AnnouncementPage"
|
||||||
Content="{shcm:ResourceString Name=ViewAnnouncementHeader}"
|
Content="{shcm:ResourceString Name=ViewAnnouncementHeader}"
|
||||||
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/Announcement.png}"/>
|
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/Announcement.png}"/>
|
||||||
<NavigationViewItem
|
|
||||||
shvh:NavHelper.NavigateTo="shvp:FeedbackPage"
|
|
||||||
Content="{shcm:ResourceString Name=ViewFeedbackHeader}"
|
|
||||||
Icon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/Feedback.png}"/>
|
|
||||||
|
|
||||||
<NavigationViewItemHeader Content="{shcm:ResourceString Name=ViewToolHeader}"/>
|
<NavigationViewItemHeader Content="{shcm:ResourceString Name=ViewToolHeader}"/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,257 +0,0 @@
|
|||||||
<shc:ScopedPage
|
|
||||||
x:Class="Snap.Hutao.View.Page.FeedbackPage"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:clw="using:CommunityToolkit.Labs.WinUI"
|
|
||||||
xmlns:cwc="using:CommunityToolkit.WinUI.Controls"
|
|
||||||
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:mxic="using:Microsoft.Xaml.Interactions.Core"
|
|
||||||
xmlns:shc="using:Snap.Hutao.Control"
|
|
||||||
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
|
|
||||||
xmlns:shci="using:Snap.Hutao.Control.Image"
|
|
||||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
|
||||||
xmlns:shvf="using:Snap.Hutao.ViewModel.Feedback"
|
|
||||||
d:DataContext="{d:DesignInstance shvf:FeedbackViewModel}"
|
|
||||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
|
||||||
mc:Ignorable="d">
|
|
||||||
|
|
||||||
<mxi:Interaction.Behaviors>
|
|
||||||
<shcb:InvokeCommandOnLoadedBehavior Command="{Binding OpenUICommand}"/>
|
|
||||||
</mxi:Interaction.Behaviors>
|
|
||||||
|
|
||||||
<Grid>
|
|
||||||
<SplitView
|
|
||||||
DisplayMode="Inline"
|
|
||||||
IsPaneOpen="True"
|
|
||||||
OpenPaneLength="400"
|
|
||||||
PaneBackground="{x:Null}"
|
|
||||||
PanePlacement="Right">
|
|
||||||
<SplitView.Pane>
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="16" Spacing="3">
|
|
||||||
<cwc:SettingsExpander
|
|
||||||
Description="{Binding RuntimeOptions.Version}"
|
|
||||||
Header="{shcm:ResourceString Name=AppName}"
|
|
||||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
|
||||||
IsExpanded="True">
|
|
||||||
<cwc:SettingsExpander.Items>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
ActionIcon="{shcm:FontIcon Glyph=}"
|
|
||||||
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingCopyDeviceIdAction}"
|
|
||||||
Command="{Binding CopyDeviceIdCommand}"
|
|
||||||
Description="{Binding RuntimeOptions.DeviceId}"
|
|
||||||
Header="{shcm:ResourceString Name=ViewPageSettingDeviceIdHeader}"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
<cwc:SettingsCard Description="{Binding IPInformation}" Header="{shcm:ResourceString Name=ViewPageSettingDeviceIpHeader}"/>
|
|
||||||
<cwc:SettingsCard Description="{Binding RuntimeOptions.WebView2Version}" Header="{shcm:ResourceString Name=ViewPageSettingWebview2Header}"/>
|
|
||||||
</cwc:SettingsExpander.Items>
|
|
||||||
</cwc:SettingsExpander>
|
|
||||||
<cwc:SettingsExpander
|
|
||||||
Description="{shcm:ResourceString Name=ViewPageFeedbackEngageWithUsDescription}"
|
|
||||||
Header="{shcm:ResourceString Name=ViewPageFeedbackCommonLinksHeader}"
|
|
||||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
|
||||||
IsExpanded="True">
|
|
||||||
<cwc:SettingsExpander.Items>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://github.com/DGP-Studio/Snap.Hutao/issues/new/choose"
|
|
||||||
Description="{shcm:ResourceString Name=ViewPageFeedbackGithubIssuesDescription}"
|
|
||||||
Header="GitHub Issues"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://github.com/orgs/DGP-Studio/projects/2"
|
|
||||||
Description="{shcm:ResourceString Name=ViewPageFeedbackRoadmapDescription}"
|
|
||||||
Header="GitHub Projects"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://status.hut.ao"
|
|
||||||
Description="{shcm:ResourceString Name=ViewPageFeedbackServerStatusDescription}"
|
|
||||||
Header="{shcm:ResourceString Name=ViewPageFeedbackServerStatusHeader}"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
</cwc:SettingsExpander.Items>
|
|
||||||
</cwc:SettingsExpander>
|
|
||||||
<cwc:SettingsExpander
|
|
||||||
Header="{shcm:ResourceString Name=ViewPageFeedbackFeatureGuideHeader}"
|
|
||||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
|
||||||
IsExpanded="True">
|
|
||||||
<cwc:SettingsExpander.Items>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://hut.ao/features/dashboard.html"
|
|
||||||
Header="{shcm:ResourceString Name=ViewAnnouncementHeader}"
|
|
||||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/Announcement.png}"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://hut.ao/features/game-launcher.html"
|
|
||||||
Header="{shcm:ResourceString Name=ViewLaunchGameHeader}"
|
|
||||||
IsClickEnabled="True">
|
|
||||||
<cwc:SettingsCard.HeaderIcon>
|
|
||||||
<!-- This icon is not a square -->
|
|
||||||
<BitmapIcon
|
|
||||||
Width="24"
|
|
||||||
Height="24"
|
|
||||||
ShowAsMonochrome="False"
|
|
||||||
UriSource="ms-appx:///Resource/Navigation/LaunchGame.png"/>
|
|
||||||
</cwc:SettingsCard.HeaderIcon>
|
|
||||||
</cwc:SettingsCard>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://hut.ao/features/wish-export.html"
|
|
||||||
Header="{shcm:ResourceString Name=ViewGachaLogHeader}"
|
|
||||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/GachaLog.png}"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://hut.ao/features/achievements.html"
|
|
||||||
Header="{shcm:ResourceString Name=ViewAchievementHeader}"
|
|
||||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/Achievement.png}"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://hut.ao/features/real-time-notes.html"
|
|
||||||
Header="{shcm:ResourceString Name=ViewDailyNoteHeader}"
|
|
||||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/DailyNote.png}"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://hut.ao/features/character-data.html"
|
|
||||||
Header="{shcm:ResourceString Name=ViewAvatarPropertyHeader}"
|
|
||||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/AvatarProperty.png}"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://hut.ao/features/hutao-API.html"
|
|
||||||
Header="{shcm:ResourceString Name=ViewSpiralAbyssHeader}"
|
|
||||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/SpiralAbyss.png}"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://hut.ao/features/develop-plan.html"
|
|
||||||
Header="{shcm:ResourceString Name=ViewCultivationHeader}"
|
|
||||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/Cultivation.png}"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://hut.ao/features/character-wiki.html"
|
|
||||||
Header="{shcm:ResourceString Name=ViewWikiAvatarHeader}"
|
|
||||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/WikiAvatar.png}"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://hut.ao/features/weapon-wiki.html"
|
|
||||||
Header="{shcm:ResourceString Name=ViewWikiWeaponHeader}"
|
|
||||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/WikiWeapon.png}"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://hut.ao/features/monster-wiki.html"
|
|
||||||
Header="{shcm:ResourceString Name=ViewWikiMonsterHeader}"
|
|
||||||
HeaderIcon="{shcm:BitmapIcon Source=ms-appx:///Resource/Navigation/WikiMonster.png}"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
<cwc:SettingsCard
|
|
||||||
Padding="{ThemeResource SettingsExpanderItemHasIconPadding}"
|
|
||||||
Command="{Binding NavigateToUriCommand}"
|
|
||||||
CommandParameter="https://hut.ao/features/hutao-settings.html"
|
|
||||||
Header="{shcm:ResourceString Name=ViewSettingHeader}"
|
|
||||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
|
||||||
IsClickEnabled="True"/>
|
|
||||||
</cwc:SettingsExpander.Items>
|
|
||||||
</cwc:SettingsExpander>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</SplitView.Pane>
|
|
||||||
<Grid>
|
|
||||||
<Grid
|
|
||||||
Padding="16,16,0,16"
|
|
||||||
RowSpacing="8"
|
|
||||||
Visibility="{Binding IsInitialized, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="auto"/>
|
|
||||||
<RowDefinition/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<AutoSuggestBox
|
|
||||||
Grid.Row="0"
|
|
||||||
Height="36"
|
|
||||||
Margin="0,0,0,8"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalContentAlignment="Center"
|
|
||||||
PlaceholderText="{shcm:ResourceString Name=ViewPageFeedbackAutoSuggestBoxPlaceholder}"
|
|
||||||
QueryIcon="{shcm:FontIcon Glyph=}"
|
|
||||||
Style="{StaticResource DefaultAutoSuggestBoxStyle}"
|
|
||||||
Text="{Binding SearchText, Mode=TwoWay}">
|
|
||||||
<mxi:Interaction.Behaviors>
|
|
||||||
<mxic:EventTriggerBehavior EventName="QuerySubmitted">
|
|
||||||
<mxic:InvokeCommandAction Command="{Binding SearchDocumentCommand}" CommandParameter="{Binding SearchText}"/>
|
|
||||||
</mxic:EventTriggerBehavior>
|
|
||||||
</mxi:Interaction.Behaviors>
|
|
||||||
</AutoSuggestBox>
|
|
||||||
<StackPanel
|
|
||||||
Grid.Row="1"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Visibility="{Binding SearchResults.Count, Converter={StaticResource Int32ToVisibilityRevertConverter}}">
|
|
||||||
<shci:CachedImage
|
|
||||||
Height="120"
|
|
||||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
|
|
||||||
EnableLazyLoading="False"
|
|
||||||
Source="{StaticResource UI_EmotionIcon52}"/>
|
|
||||||
<TextBlock
|
|
||||||
Margin="0,5,0,21"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
|
||||||
Text="暂无搜索结果"/>
|
|
||||||
</StackPanel>
|
|
||||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Hidden">
|
|
||||||
<ItemsControl
|
|
||||||
ItemContainerTransitions="{ThemeResource ListViewLikeThemeTransitions}"
|
|
||||||
ItemsPanel="{ThemeResource StackPanelSpacing8Template}"
|
|
||||||
ItemsSource="{Binding SearchResults}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<Border Style="{ThemeResource BorderCardStyle}">
|
|
||||||
<HyperlinkButton
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
HorizontalContentAlignment="Stretch"
|
|
||||||
NavigateUri="{Binding Url}">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition/>
|
|
||||||
<ColumnDefinition Width="auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<BreadcrumbBar
|
|
||||||
Grid.Column="0"
|
|
||||||
Margin="4,8,8,4"
|
|
||||||
IsHitTestVisible="False"
|
|
||||||
ItemsSource="{Binding Hierarchy.DisplayLevels}"/>
|
|
||||||
</Grid>
|
|
||||||
</HyperlinkButton>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<clw:Shimmer
|
|
||||||
CornerRadius="0"
|
|
||||||
IsActive="{Binding IsInitialized, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}"
|
|
||||||
Visibility="{Binding IsInitialized, Converter={StaticResource BoolToVisibilityRevertConverter}, Mode=OneWay}"/>
|
|
||||||
</Grid>
|
|
||||||
</SplitView>
|
|
||||||
</Grid>
|
|
||||||
</shc:ScopedPage>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Control;
|
|
||||||
using Snap.Hutao.ViewModel.Feedback;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.View.Page;
|
|
||||||
|
|
||||||
internal sealed partial class FeedbackPage : ScopedPage
|
|
||||||
{
|
|
||||||
public FeedbackPage()
|
|
||||||
{
|
|
||||||
InitializeWith<FeedbackViewModel>();
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -171,8 +171,8 @@
|
|||||||
Text="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeWarning}"/>
|
Text="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeWarning}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</cwc:SettingsCard.Description>
|
</cwc:SettingsCard.Description>
|
||||||
<StackPanel Orientation="Horizontal" Spacing="{ThemeResource SettingsCardContentControlSpacing}">
|
<StackPanel Orientation="Horizontal">
|
||||||
<shvc:Elevation Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
<shvc:Elevation Margin="0,0,36,0" Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||||
<shc:SizeRestrictedContentControl>
|
<shc:SizeRestrictedContentControl>
|
||||||
<shccs:ComboBox2
|
<shccs:ComboBox2
|
||||||
DisplayMemberPath="DisplayName"
|
DisplayMemberPath="DisplayName"
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameWindowsHDRDescription}"
|
Description="{shcm:ResourceString Name=ViewPageLaunchGameWindowsHDRDescription}"
|
||||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameWindowsHDRHeader}"
|
Header="{shcm:ResourceString Name=ViewPageLaunchGameWindowsHDRHeader}"
|
||||||
HeaderIcon="{shcm:FontIcon Glyph=}">
|
HeaderIcon="{shcm:FontIcon Glyph=}">
|
||||||
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsWindowsHDREnabled, Mode=TwoWay}"/>
|
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsWindowsHDREnabled, Mode=TwoWay}"/>
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
|
|
||||||
<!-- 进程 -->
|
<!-- 进程 -->
|
||||||
@@ -213,88 +213,81 @@
|
|||||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsDescription}"
|
Description="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsDescription}"
|
||||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsHeader}"
|
Header="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsHeader}"
|
||||||
HeaderIcon="{shcm:FontIcon Glyph=}">
|
HeaderIcon="{shcm:FontIcon Glyph=}">
|
||||||
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsEnabled, Mode=TwoWay}"/>
|
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsEnabled, Mode=TwoWay}"/>
|
||||||
<cwc:SettingsExpander.Items>
|
<cwc:SettingsExpander.Items>
|
||||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceExclusiveDescription}" Header="-window-mode exclusive">
|
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceExclusiveDescription}" Header="-window-mode exclusive">
|
||||||
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsExclusive, Mode=TwoWay}"/>
|
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsExclusive, Mode=TwoWay}"/>
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceFullscreenDescription}" Header="-screen-fullscreen">
|
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceFullscreenDescription}" Header="-screen-fullscreen">
|
||||||
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsFullScreen, Mode=TwoWay}"/>
|
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsFullScreen, Mode=TwoWay}"/>
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceBorderlessDescription}" Header="-popupwindow">
|
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceBorderlessDescription}" Header="-popupwindow">
|
||||||
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsBorderless, Mode=TwoWay}"/>
|
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsBorderless, Mode=TwoWay}"/>
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceCloudThirdPartyMobileDescription}" Header="-platform_type CLOUD_THIRD_PARTY_MOBILE">
|
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceCloudThirdPartyMobileDescription}" Header="-platform_type CLOUD_THIRD_PARTY_MOBILE">
|
||||||
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsUseCloudThirdPartyMobile, Mode=TwoWay}"/>
|
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsUseCloudThirdPartyMobile, Mode=TwoWay}"/>
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioDescription}" Header="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioHeader}">
|
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioDescription}" Header="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioHeader}">
|
||||||
<shc:SizeRestrictedContentControl Margin="0,0,130,0" VerticalAlignment="Center">
|
<shc:SizeRestrictedContentControl Margin="0,0,136,0">
|
||||||
<ComboBox
|
<ComboBox
|
||||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
|
|
||||||
ItemsSource="{Binding LaunchOptions.AspectRatios}"
|
ItemsSource="{Binding LaunchOptions.AspectRatios}"
|
||||||
PlaceholderText="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioPlaceHolder}"
|
PlaceholderText="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioPlaceHolder}"
|
||||||
SelectedItem="{Binding LaunchOptions.SelectedAspectRatio, Mode=TwoWay}"/>
|
SelectedItem="{Binding LaunchOptions.SelectedAspectRatio, Mode=TwoWay}"/>
|
||||||
</shc:SizeRestrictedContentControl>
|
</shc:SizeRestrictedContentControl>
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenWidthDescription}" Header="-screen-width">
|
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenWidthDescription}" Header="-screen-width">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||||
<NumberBox
|
<NumberBox
|
||||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
|
Width="156"
|
||||||
Padding="12,6,0,0"
|
Padding="12,6,0,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
IsEnabled="{Binding LaunchOptions.IsScreenWidthEnabled}"
|
IsEnabled="{Binding LaunchOptions.IsScreenWidthEnabled}"
|
||||||
Value="{Binding LaunchOptions.ScreenWidth, Mode=TwoWay}"/>
|
Value="{Binding LaunchOptions.ScreenWidth, Mode=TwoWay}"/>
|
||||||
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsScreenWidthEnabled, Mode=TwoWay}"/>
|
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsScreenWidthEnabled, Mode=TwoWay}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenHeightDescription}" Header="-screen-height">
|
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenHeightDescription}" Header="-screen-height">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||||
<NumberBox
|
<NumberBox
|
||||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
|
Width="156"
|
||||||
Padding="12,6,0,0"
|
Padding="12,6,0,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
IsEnabled="{Binding LaunchOptions.IsScreenHeightEnabled}"
|
IsEnabled="{Binding LaunchOptions.IsScreenHeightEnabled}"
|
||||||
Value="{Binding LaunchOptions.ScreenHeight, Mode=TwoWay}"/>
|
Value="{Binding LaunchOptions.ScreenHeight, Mode=TwoWay}"/>
|
||||||
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsScreenHeightEnabled, Mode=TwoWay}"/>
|
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsScreenHeightEnabled, Mode=TwoWay}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameMonitorsDescription}" Header="-monitor">
|
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameMonitorsDescription}" Header="-monitor">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||||
<Button
|
<shc:SizeRestrictedContentControl>
|
||||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
|
|
||||||
Command="{Binding IdentifyMonitorsCommand}"
|
|
||||||
Content="{shcm:ResourceString Name=ViewModelLaunchGameIdentifyMonitorsAction}"/>
|
|
||||||
<shc:SizeRestrictedContentControl VerticalAlignment="Center">
|
|
||||||
<ComboBox
|
<ComboBox
|
||||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
|
|
||||||
DisplayMemberPath="Name"
|
DisplayMemberPath="Name"
|
||||||
IsEnabled="{Binding LaunchOptions.IsMonitorEnabled}"
|
IsEnabled="{Binding LaunchOptions.IsMonitorEnabled}"
|
||||||
ItemsSource="{Binding LaunchOptions.Monitors}"
|
ItemsSource="{Binding LaunchOptions.Monitors}"
|
||||||
SelectedItem="{Binding LaunchOptions.Monitor, Mode=TwoWay}"/>
|
SelectedItem="{Binding LaunchOptions.Monitor, Mode=TwoWay}"/>
|
||||||
</shc:SizeRestrictedContentControl>
|
</shc:SizeRestrictedContentControl>
|
||||||
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsMonitorEnabled, Mode=TwoWay}"/>
|
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsMonitorEnabled, Mode=TwoWay}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
</cwc:SettingsExpander.Items>
|
</cwc:SettingsExpander.Items>
|
||||||
</cwc:SettingsExpander>
|
</cwc:SettingsExpander>
|
||||||
<cwc:SettingsCard
|
<cwc:SettingsCard
|
||||||
Padding="{ThemeResource SettingsCardAlignSettingsExpanderPadding}"
|
|
||||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsDescription}"
|
Description="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsDescription}"
|
||||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsHeader}"
|
Header="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsHeader}"
|
||||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||||
IsEnabled="{Binding RuntimeOptions.IsElevated}"
|
IsEnabled="{Binding RuntimeOptions.IsElevated}"
|
||||||
Visibility="{Binding LaunchOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}">
|
Visibility="{Binding LaunchOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
<StackPanel Orientation="Horizontal">
|
||||||
<shvc:Elevation Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
<shvc:Elevation Margin="0,0,36,0" Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||||
<NumberBox
|
<NumberBox
|
||||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
|
MinWidth="156"
|
||||||
Padding="10,8,0,0"
|
Padding="10,8,0,0"
|
||||||
Maximum="720"
|
Maximum="720"
|
||||||
Minimum="60"
|
Minimum="60"
|
||||||
SpinButtonPlacementMode="Inline"
|
SpinButtonPlacementMode="Inline"
|
||||||
Value="{Binding LaunchOptions.TargetFps, Mode=TwoWay}"/>
|
Value="{Binding LaunchOptions.TargetFps, Mode=TwoWay}"/>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
|
Width="120"
|
||||||
IsOn="{Binding LaunchOptions.UnlockFps, Mode=TwoWay}"
|
IsOn="{Binding LaunchOptions.UnlockFps, Mode=TwoWay}"
|
||||||
OffContent="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsOff}"
|
OffContent="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsOff}"
|
||||||
OnContent="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsOn}"/>
|
OnContent="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsOn}"/>
|
||||||
@@ -307,13 +300,13 @@
|
|||||||
Description="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeDescription}"
|
Description="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeDescription}"
|
||||||
Header="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeHeader}"
|
Header="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeHeader}"
|
||||||
HeaderIcon="{shcm:FontIcon Glyph=}">
|
HeaderIcon="{shcm:FontIcon Glyph=}">
|
||||||
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.UseStarwardPlayTimeStatistics, Mode=TwoWay}"/>
|
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.UseStarwardPlayTimeStatistics, Mode=TwoWay}"/>
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
<cwc:SettingsCard
|
<cwc:SettingsCard
|
||||||
Description="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityDescription}"
|
Description="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityDescription}"
|
||||||
Header="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityHeader}"
|
Header="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityHeader}"
|
||||||
HeaderIcon="{shcm:FontIcon Glyph=}">
|
HeaderIcon="{shcm:FontIcon Glyph=}">
|
||||||
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.SetDiscordActivityWhenPlaying, Mode=TwoWay}"/>
|
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.SetDiscordActivityWhenPlaying, Mode=TwoWay}"/>
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -362,8 +355,8 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Spacing="3">
|
Spacing="3">
|
||||||
<shci:CachedImage
|
<shci:CachedImage
|
||||||
|
Width="120"
|
||||||
Height="120"
|
Height="120"
|
||||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
|
|
||||||
EnableLazyLoading="False"
|
EnableLazyLoading="False"
|
||||||
Source="{StaticResource UI_EmotionIcon445}"/>
|
Source="{StaticResource UI_EmotionIcon445}"/>
|
||||||
<TextBlock
|
<TextBlock
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
Description="{shcm:ResourceString Name=ViewPageSettingElevatedModeDescription}"
|
Description="{shcm:ResourceString Name=ViewPageSettingElevatedModeDescription}"
|
||||||
Header="{shcm:ResourceString Name=ViewPageSettingElevatedModeRestartAction}"
|
Header="{shcm:ResourceString Name=ViewPageSettingElevatedModeRestartAction}"
|
||||||
IsClickEnabled="True"
|
IsClickEnabled="True"
|
||||||
IsEnabled="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolNegationConverter}}"/>
|
IsEnabled="{Binding HutaoOptions.IsElevated, Converter={StaticResource BoolNegationConverter}}"/>
|
||||||
<cwc:SettingsCard
|
<cwc:SettingsCard
|
||||||
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutAction}"
|
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutAction}"
|
||||||
Command="{Binding CreateDesktopShortcutCommand}"
|
Command="{Binding CreateDesktopShortcutCommand}"
|
||||||
@@ -111,6 +111,23 @@
|
|||||||
</shch:ScrollViewerHelper.RightPanel>
|
</shch:ScrollViewerHelper.RightPanel>
|
||||||
<Grid Padding="16" HorizontalAlignment="Left">
|
<Grid Padding="16" HorizontalAlignment="Left">
|
||||||
<StackPanel Grid.Column="0" Spacing="{StaticResource SettingsCardSpacing}">
|
<StackPanel Grid.Column="0" Spacing="{StaticResource SettingsCardSpacing}">
|
||||||
|
<cwc:SettingsExpander
|
||||||
|
Description="{Binding HutaoOptions.Version}"
|
||||||
|
Header="{shcm:ResourceString Name=AppName}"
|
||||||
|
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||||
|
IsExpanded="True">
|
||||||
|
<cwc:SettingsExpander.Items>
|
||||||
|
<cwc:SettingsCard
|
||||||
|
ActionIcon="{shcm:FontIcon Glyph=}"
|
||||||
|
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingCopyDeviceIdAction}"
|
||||||
|
Command="{Binding CopyDeviceIdCommand}"
|
||||||
|
Description="{Binding HutaoOptions.DeviceId}"
|
||||||
|
Header="{shcm:ResourceString Name=ViewPageSettingDeviceIdHeader}"
|
||||||
|
IsClickEnabled="True"/>
|
||||||
|
<cwc:SettingsCard Description="{Binding IPInformation}" Header="{shcm:ResourceString Name=ViewPageSettingDeviceIpHeader}"/>
|
||||||
|
<cwc:SettingsCard Description="{Binding HutaoOptions.WebView2Version}" Header="{shcm:ResourceString Name=ViewPageSettingWebview2Header}"/>
|
||||||
|
</cwc:SettingsExpander.Items>
|
||||||
|
</cwc:SettingsExpander>
|
||||||
<!--
|
<!--
|
||||||
https://github.com/DGP-Studio/Snap.Hutao/issues/1072
|
https://github.com/DGP-Studio/Snap.Hutao/issues/1072
|
||||||
ItemsRepeater will behave abnormal if no direct scrollhost wrapping it
|
ItemsRepeater will behave abnormal if no direct scrollhost wrapping it
|
||||||
@@ -196,7 +213,7 @@
|
|||||||
<shc:SizeRestrictedContentControl>
|
<shc:SizeRestrictedContentControl>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
DisplayMemberPath="Name"
|
DisplayMemberPath="Name"
|
||||||
ItemsSource="{Binding CultureOptions.Cultures}"
|
ItemsSource="{Binding AppOptions.Cultures}"
|
||||||
SelectedItem="{Binding SelectedCulture, Mode=TwoWay}"/>
|
SelectedItem="{Binding SelectedCulture, Mode=TwoWay}"/>
|
||||||
</shc:SizeRestrictedContentControl>
|
</shc:SizeRestrictedContentControl>
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
@@ -399,7 +416,7 @@
|
|||||||
<cwc:SettingsCard
|
<cwc:SettingsCard
|
||||||
Header="{shcm:ResourceString Name=ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader}"
|
Header="{shcm:ResourceString Name=ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader}"
|
||||||
HeaderIcon="{shcm:FontIcon Glyph=}"
|
HeaderIcon="{shcm:FontIcon Glyph=}"
|
||||||
IsEnabled="{Binding RuntimeOptions.IsElevated}">
|
IsEnabled="{Binding HutaoOptions.IsElevated}">
|
||||||
<cwc:SettingsCard.Description>
|
<cwc:SettingsCard.Description>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock
|
<TextBlock
|
||||||
@@ -410,7 +427,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</cwc:SettingsCard.Description>
|
</cwc:SettingsCard.Description>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<shvc:Elevation Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
<shvc:Elevation Visibility="{Binding HutaoOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
|
||||||
<ToggleSwitch Width="120" IsOn="{Binding IsAdvancedLaunchOptionsEnabled, Mode=TwoWay}"/>
|
<ToggleSwitch Width="120" IsOn="{Binding IsAdvancedLaunchOptionsEnabled, Mode=TwoWay}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
|
|||||||
@@ -61,6 +61,31 @@
|
|||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</StackPanel.Resources>
|
</StackPanel.Resources>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Margin="4,0"
|
||||||
|
Padding="6,6"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
HorizontalContentAlignment="Stretch"
|
||||||
|
Command="{Binding OpenDocumentationCommand}">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="auto"/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<BitmapIcon
|
||||||
|
Grid.Column="0"
|
||||||
|
Width="24"
|
||||||
|
Height="24"
|
||||||
|
ShowAsMonochrome="False"
|
||||||
|
UriSource="ms-appx:///Resource/Navigation/Documentation.png"/>
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="13,0,0,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{shcm:ResourceString Name=ViewUserDocumentationHeader}"/>
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button MaxHeight="40" Margin="4,0">
|
<Button MaxHeight="40" Margin="4,0">
|
||||||
<Button.Content>
|
<Button.Content>
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user