Compare commits

..

34 Commits

Author SHA1 Message Date
DismissedLight
f1d9787e45 fix method call 2024-01-08 22:18:07 +08:00
DismissedLight
5f9b4a7cb2 refactor RegistryWatcher 2024-01-08 21:55:59 +08:00
qhy040404
8710150897 use reflect to reduce code size 2024-01-08 20:19:05 +08:00
qhy040404
92c1b12764 dynamic proxy 2024-01-08 18:24:02 +08:00
Lightczx
d73bd557f3 remove settings appearance backdrop transparent 2024-01-08 16:52:37 +08:00
Lightczx
777d7d1056 remove winrt marshaller 2024-01-08 16:30:53 +08:00
Lightczx
1a944dae9c add transparent backdrop 2024-01-08 15:16:41 +08:00
Lightczx
a26c52ba97 typo 2024-01-08 11:43:45 +08:00
Lightczx
5fab03d57e Update FeedbackPage.xaml 2024-01-08 11:41:23 +08:00
Lightczx
e8a459cb41 refine #1039 2024-01-08 11:36:16 +08:00
DismissedLight
04df5a7bf1 impl #1039 2024-01-07 23:23:59 +08:00
DismissedLight
1ebcc2fc89 add documentation client 2024-01-07 14:49:02 +08:00
DismissedLight
e9917e788d Merge pull request #1273 from DGP-Studio/feat/identify_monitor 2024-01-06 23:39:41 +08:00
DismissedLight
9665876d52 code style 2 2024-01-06 23:38:26 +08:00
DismissedLight
8921816873 code style 2024-01-06 22:57:25 +08:00
DismissedLight
2698761594 fix convert game path 2024-01-06 20:03:14 +08:00
qhy040404
3ae4210ca0 add i18n 2024-01-06 18:32:39 +08:00
qhy040404
2f5e0cbe39 impl #1261 2024-01-06 18:25:10 +08:00
DismissedLight
d3444a9435 typo 2024-01-06 15:22:40 +08:00
DismissedLight
8b6f95c3d9 add package convert check 2024-01-06 15:21:51 +08:00
DismissedLight
88b8335e5b Merge pull request #1271 from DGP-Studio/feat/refresh_data_size 2024-01-05 23:52:55 +08:00
qhy040404
061aba715b refresh data folder size after deleting server cache 2024-01-05 23:50:28 +08:00
DismissedLight
da80631b72 code style 2024-01-05 23:28:35 +08:00
DismissedLight
97acf872bc remove status when game exited 2024-01-05 23:28:05 +08:00
DismissedLight
addaf1a9e3 Merge pull request #1270 from DGP-Studio/feat/launch-pipeline 2024-01-05 22:46:00 +08:00
DismissedLight
76183901da clean up 2024-01-05 22:33:10 +08:00
Lightczx
87ee81e7fa add handlers 2024-01-05 17:29:30 +08:00
DismissedLight
f2f858de15 create infrastructure 2024-01-04 22:51:58 +08:00
DismissedLight
c434521004 Merge pull request #1265 from DGP-Studio/fix/schedule 2024-01-04 16:03:54 +08:00
Lightczx
27ed2cefc1 fix #1242 2024-01-04 16:01:52 +08:00
qhy040404
6dc1e664b0 add task register check and delete script if register is failed 2024-01-04 13:32:43 +08:00
DismissedLight
51c3dde24b Merge pull request #1263 from DSakura207/main 2024-01-04 09:18:23 +08:00
DSakura207
2d497faaa5 Update Contributing.md 2024-01-03 18:35:47 -06:00
DSakura207
4783934b92 Add .vsconfig for installing workloads and extensions 2024-01-03 18:17:09 -06:00
123 changed files with 2299 additions and 1033 deletions

View File

@@ -4,13 +4,15 @@
### Setup Snap.Hutao Project
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/)
2. Open Visual Studio Installer to complete Visual Studio installation
- You need to install `.NET desktop development`, `Desktop development with C++` and `Universal Windows Platform development` components
3. Install `Single-project MSIX Packaging Tools for VS 2022` provided by Microsoft in Visual Studio marketplace
4. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device
5. Switch git branch to `develop`
6. Open project solution with your Visual Studio and then you are ready to go
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/).
- No need to select workloads; Visual Studio will handle it automatically.
- Close Visual Studio Installer to ensure a smooth installation experience for workloads.
- If using Visual Studio 2022 17.9 preview, skip step 5, as automatic extension installation is supported in this version.
2. 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.
4. Open the project solution with your Visual Studio. Visual Studio will prompt you to install the necessary workloads, closing and reopening automatically.
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

View File

@@ -124,9 +124,6 @@ dotnet_diagnostic.SA1623.severity = none
# SA1636: File header copyright text should match
dotnet_diagnostic.SA1636.severity = none
# SA1414: Tuple types in signatures should have element names
dotnet_diagnostic.SA1414.severity = none
# SA0001: XML comment analysis disabled
dotnet_diagnostic.SA0001.severity = none
csharp_style_prefer_parameter_null_checking = true:suggestion
@@ -325,7 +322,6 @@ dotnet_diagnostic.CA2227.severity = suggestion
# CA2251: 使用 “string.Equals”
dotnet_diagnostic.CA2251.severity = suggestion
csharp_style_prefer_primary_constructors = true:suggestion
dotnet_diagnostic.SA1010.severity = none
[*.vb]
#### 命名样式 ####

11
src/Snap.Hutao/.vsconfig Normal file
View File

@@ -0,0 +1,11 @@
{
"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"
]
}

View File

@@ -1,4 +1,15 @@
// COMCTL32
// ADVAPI32
RegCloseKey
RegOpenKeyExW
RegNotifyChangeKeyValue
REG_NOTIFY_FILTER
HKEY_CLASSES_ROOT
HKEY_CURRENT_USER
HKEY_LOCAL_MACHINE
HKEY_USERS
HKEY_CURRENT_CONFIG
// COMCTL32
DefSubclassProc
RemoveWindowSubclass
SetWindowSubclass
@@ -47,12 +58,14 @@ GetCursorPos
GetDC
GetDpiForWindow
GetForegroundWindow
GetWindowLongPtrW
GetWindowPlacement
GetWindowThreadProcessId
ReleaseDC
RegisterHotKey
SendInput
SetForegroundWindow
SetWindowLongPtrW
UnregisterHotKey
// COM
@@ -74,6 +87,7 @@ E_FAIL
INFINITE
RPC_E_WRONG_THREAD
MAX_PATH
WM_ERASEBKGND
WM_GETMINMAXINFO
WM_HOTKEY
WM_NCRBUTTONDOWN
@@ -89,6 +103,7 @@ LPTHREAD_START_ROUTINE
// UI.WindowsAndMessaging
MINMAXINFO
WINDOW_EX_STYLE
// System.Com
CWMO_FLAGS

View File

@@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,72 +0,0 @@
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);
}
}
}

View File

@@ -8,6 +8,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9A95A964-04B1-477A-BDE7-505525B3CAD8}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.vsconfig = .vsconfig
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
@@ -87,11 +88,11 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
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"]}
RESX_ShowErrorsInErrorList = False
RESX_SortFileContentOnSave = True
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
RESX_NeutralResourcesLanguage = zh-CN
RESX_AutoApplyExistingTranslations = False
EndGlobalSection
EndGlobal

View File

@@ -7,6 +7,7 @@ namespace Snap.Hutao.Control;
[TemplateVisualState(Name = "LoadingIn", GroupName = "CommonStates")]
[TemplateVisualState(Name = "LoadingOut", GroupName = "CommonStates")]
[TemplatePart(Name = "ContentGrid", Type = typeof(FrameworkElement))]
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));

View File

@@ -3,7 +3,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shc="using:Snap.Hutao.Control">
<Style TargetType="shc:Loading">
<Style BasedOn="{StaticResource DefaultLoadingStyle}" TargetType="shc:Loading"/>
<Style x:Key="DefaultLoadingStyle" TargetType="shc:Loading">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>

View File

@@ -18,12 +18,15 @@ internal sealed class FontIconExtension : MarkupExtension
/// </summary>
public string Glyph { get; set; } = default!;
public double FontSize { get; set; } = 12;
/// <inheritdoc/>
protected override object ProvideValue()
{
return new FontIcon()
{
Glyph = Glyph,
FontSize = FontSize,
};
}
}

View File

@@ -20,6 +20,9 @@
<ItemsPanelTemplate x:Key="StackPanelSpacing4Template">
<StackPanel Spacing="4"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="StackPanelSpacing8Template">
<StackPanel Spacing="8"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="UniformGridColumns2Spacing2Template">
<cwcont:UniformGrid
ColumnSpacing="2"

View File

@@ -6,6 +6,12 @@
<x:Double x:Key="SettingsCardWrapNoIconThreshold">0</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
x:Key="SettingsSectionHeaderTextBlockStyle"

View File

@@ -27,6 +27,7 @@
<!-- EmotionIcon -->
<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_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>

View File

@@ -2,9 +2,11 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Core.IO.Http.DynamicProxy;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Service;
using System.Globalization;
using System.Net.Http;
using System.Runtime.CompilerServices;
using Windows.Globalization;
@@ -41,6 +43,7 @@ internal static class DependencyInjection
serviceProvider.InitializeConsoleWindow();
serviceProvider.InitializeCulture();
serviceProvider.InitializedDynamicHttpProxy();
return serviceProvider;
}
@@ -48,10 +51,10 @@ internal static class DependencyInjection
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InitializeCulture(this IServiceProvider serviceProvider)
{
AppOptions appOptions = serviceProvider.GetRequiredService<AppOptions>();
appOptions.PreviousCulture = CultureInfo.CurrentCulture;
CultureOptions cultureOptions = serviceProvider.GetRequiredService<CultureOptions>();
cultureOptions.SystemCulture = CultureInfo.CurrentCulture;
CultureInfo cultureInfo = appOptions.CurrentCulture;
CultureInfo cultureInfo = cultureOptions.CurrentCulture;
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
@@ -67,4 +70,9 @@ internal static class DependencyInjection
{
_ = serviceProvider.GetRequiredService<ConsoleWindowLifeTime>();
}
private static void InitializedDynamicHttpProxy(this IServiceProvider serviceProvider)
{
HttpClient.DefaultProxy = serviceProvider.GetRequiredService<DynamicHttpProxy>();
}
}

View File

@@ -38,8 +38,8 @@ internal static class IocConfiguration
private static void AddDbContextCore(IServiceProvider provider, DbContextOptionsBuilder builder)
{
RuntimeOptions hutaoOptions = provider.GetRequiredService<RuntimeOptions>();
string dbFile = System.IO.Path.Combine(hutaoOptions.DataFolder, "Userdata.db");
RuntimeOptions runtimeOptions = provider.GetRequiredService<RuntimeOptions>();
string dbFile = System.IO.Path.Combine(runtimeOptions.DataFolder, "Userdata.db");
string sqlConnectionString = $"Data Source={dbFile}";
// Temporarily create a context

View File

@@ -29,10 +29,10 @@ internal static partial class IocHttpClientConfiguration
/// <param name="client">配置后的客户端</param>
private static void DefaultConfiguration(IServiceProvider serviceProvider, HttpClient client)
{
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
client.Timeout = Timeout.InfiniteTimeSpan;
client.DefaultRequestHeaders.UserAgent.ParseAdd(hutaoOptions.UserAgent);
client.DefaultRequestHeaders.UserAgent.ParseAdd(runtimeOptions.UserAgent);
}
/// <summary>

View File

@@ -0,0 +1,87 @@
// 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();
}
}

View File

@@ -11,11 +11,14 @@ namespace Snap.Hutao.Core.IO.Ini;
[HighQuality]
internal static class IniSerializer
{
/// <summary>
/// 反序列化
/// </summary>
/// <param name="fileStream">文件流</param>
/// <returns>Ini 元素集合</returns>
public static List<IniElement> DeserializeFromFile(string filePath)
{
using (FileStream readStream = File.OpenRead(filePath))
{
return Deserialize(readStream);
}
}
public static List<IniElement> Deserialize(FileStream fileStream)
{
List<IniElement> results = [];
@@ -50,11 +53,14 @@ internal static class IniSerializer
return results;
}
/// <summary>
/// 序列化
/// </summary>
/// <param name="fileStream">写入的流</param>
/// <param name="elements">元素</param>
public static void SerializeToFile(string filePath, IEnumerable<IniElement> elements)
{
using (FileStream writeStream = File.Create(filePath))
{
Serialize(writeStream, elements);
}
}
public static void Serialize(FileStream fileStream, IEnumerable<IniElement> elements)
{
using (StreamWriter writer = new(fileStream))

View File

@@ -190,7 +190,7 @@ internal sealed partial class Activation : IActivation
serviceProvider
.GetRequiredService<IDiscordService>()
.SetNormalActivity()
.SetNormalActivityAsync()
.SafeForget();
}

View File

@@ -39,6 +39,11 @@ internal sealed class ScheduleTaskInterop : IScheduleTaskInterop
}
catch (Exception)
{
if (WScriptExists(DailyNoteRefreshScriptName, out string fullPath))
{
File.Delete(fullPath);
}
return false;
}
}

View File

@@ -69,4 +69,9 @@ internal static class StructMarshal
{
return new(point.X, point.Y, size.Width, size.Height);
}
public static SizeInt32 SizeInt32(RectInt32 rect)
{
return new(rect.Width, rect.Height);
}
}

View File

@@ -8,12 +8,12 @@ internal readonly struct Delay
/// <summary>
/// 随机延迟
/// </summary>
/// <param name="minMilliSeconds">最小,闭</param>
/// <param name="maxMilliSeconds">最小,开</param>
/// <param name="min">最小,闭</param>
/// <param name="max">最小,开</param>
/// <returns>任务</returns>
public static ValueTask Random(int minMilliSeconds, int maxMilliSeconds)
public static ValueTask RandomMilliSeconds(int min, int max)
{
return Task.Delay((int)(System.Random.Shared.NextDouble() * (maxMilliSeconds - minMilliSeconds)) + minMilliSeconds).AsValueTask();
return Task.Delay((int)(System.Random.Shared.NextDouble() * (max - min)) + min).AsValueTask();
}
public static ValueTask FromSeconds(int seconds)

View File

@@ -0,0 +1,67 @@
// 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;

View File

@@ -9,6 +9,8 @@ namespace Snap.Hutao.Core.Windowing;
[HighQuality]
internal enum BackdropType
{
Transparent = -1,
/// <summary>
/// 无
/// </summary>

View File

@@ -9,6 +9,7 @@ using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service;
using System.Diagnostics;
using System.IO;
using Windows.Graphics;
using Windows.UI;
@@ -53,10 +54,10 @@ internal sealed class WindowController
private void InitializeCore()
{
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
window.AppWindow.Title = SH.FormatAppNameAndVersion(hutaoOptions.Version);
window.AppWindow.SetIcon(Path.Combine(hutaoOptions.InstalledLocation, "Assets/Logo.ico"));
window.AppWindow.Title = SH.FormatAppNameAndVersion(runtimeOptions.Version);
window.AppWindow.SetIcon(Path.Combine(runtimeOptions.InstalledLocation, "Assets/Logo.ico"));
ExtendsContentIntoTitleBar();
RecoverOrInitWindowSize();
@@ -157,6 +158,7 @@ internal sealed class WindowController
{
window.SystemBackdrop = backdropType switch
{
BackdropType.Transparent => new Backdrop.TransparentBackdrop(),
BackdropType.MicaAlt => new MicaBackdrop() { Kind = MicaKind.BaseAlt },
BackdropType.Mica => new MicaBackdrop() { Kind = MicaKind.Base },
BackdropType.Acrylic => new DesktopAcrylicBackdrop(),

View File

@@ -3,6 +3,10 @@
using Microsoft.UI.Xaml;
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;
@@ -16,4 +20,12 @@ internal static class WindowExtension
WindowController windowController = new(window, window.WindowOptions, serviceProvider);
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);
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Windowing.Backdrop;
using Snap.Hutao.Core.Windowing.HotKey;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;
@@ -110,6 +111,16 @@ internal sealed class WindowSubclass : IDisposable
hotKeyController.OnHotKeyPressed(*(HotKeyParameter*)&lParam);
break;
}
case WM_ERASEBKGND:
{
if (window.SystemBackdrop is IBackdropNeedEraseBackground)
{
return (LRESULT)(int)(BOOL)true;
}
break;
}
}
return DefSubclassProc(hwnd, uMsg, wParam, lParam);

View File

@@ -0,0 +1,19 @@
<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>

View File

@@ -0,0 +1,30 @@
// 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; }
}

View File

@@ -11,6 +11,12 @@ internal static class CollectionsNameValue
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)
{
return [.. sources.Select(x => new NameValue<TSource>(nameSelector(x), x))];

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Service;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Web.Hoyolab;
@@ -12,7 +13,7 @@ namespace Snap.Hutao.Model.InterChange.GachaLog;
/// UIGF格式的信息
/// </summary>
[HighQuality]
internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, MetadataOptions, string>
internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, CultureOptions, string>
{
/// <summary>
/// 用户Uid
@@ -65,12 +66,12 @@ internal sealed class UIGFInfo : IMappingFrom<UIGFInfo, RuntimeOptions, Metadata
[JsonPropertyName("region_time_zone")]
public int? RegionTimeZone { get; set; } = default!;
public static UIGFInfo From(RuntimeOptions runtimeOptions, MetadataOptions metadataOptions, string uid)
public static UIGFInfo From(RuntimeOptions runtimeOptions, CultureOptions cultureOptions, string uid)
{
return new()
{
Uid = uid,
Language = metadataOptions.LanguageCode,
Language = cultureOptions.LanguageCode,
ExportTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExportApp = SH.AppName,
ExportAppVersion = runtimeOptions.Version.ToString(),

View File

@@ -64,14 +64,14 @@ internal sealed class UIIFInfo
/// <returns>专用 UIGF 信息</returns>
public static UIIFInfo From(IServiceProvider serviceProvider, string uid)
{
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
return new()
{
Uid = uid,
ExportTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExportApp = SH.AppName,
ExportAppVersion = hutaoOptions.Version.ToString(),
ExportAppVersion = runtimeOptions.Version.ToString(),
UIIFVersion = UIIF.CurrentVersion,
};
}

View File

@@ -870,11 +870,23 @@
<value>文件系统权限不足,无法转换服务器</value>
</data>
<data name="ServiceGameEnsureGameResourceQueryResourceInformation" xml:space="preserve">
<value>查询游戏资源信息</value>
<value>下载游戏资源索引</value>
</data>
<data name="ServiceGameFileOperationExceptionMessage" xml:space="preserve">
<value>游戏文件操作失败:{0}</value>
</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">
<value>游戏进程已退出</value>
</data>
@@ -1298,6 +1310,9 @@
<data name="ViewDialogUserTitle" xml:space="preserve">
<value>设置 Cookie</value>
</data>
<data name="ViewFeedbackHeader" xml:space="preserve">
<value>反馈中心</value>
</data>
<data name="ViewGachaLogHeader" xml:space="preserve">
<value>祈愿记录</value>
</data>
@@ -1547,6 +1562,9 @@
<data name="ViewModelLaunchGameEnsureGameResourceFail" xml:space="preserve">
<value>切换服务器失败</value>
</data>
<data name="ViewModelLaunchGameIdentifyMonitorsAction" xml:space="preserve">
<value>识别显示器</value>
</data>
<data name="ViewModelLaunchGameMultiChannelReadFail" xml:space="preserve">
<value>无法读取游戏配置文件: {0},可能是文件不存在或权限不足</value>
</data>
@@ -1835,6 +1853,33 @@
<data name="ViewPageDailyNoteVerify" xml:space="preserve">
<value>验证当前用户与角色</value>
</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">
<value>全量刷新</value>
</data>
@@ -2591,6 +2636,9 @@
<data name="ViewSettingFolderViewOpenFolderAction" xml:space="preserve">
<value>打开文件夹</value>
</data>
<data name="ViewSettingHeader" xml:space="preserve">
<value>设置</value>
</data>
<data name="ViewSpiralAbyssAvatarAppearanceRankDescription" xml:space="preserve">
<value>角色出场率 = 本层上阵该角色次数(层内重复出现只记一次)/ 深渊记录总数</value>
</data>
@@ -2954,4 +3002,7 @@
<data name="WebResponseRequestExceptionFormat" xml:space="preserve">
<value>[{0}] 中的 [{1}] 网络请求异常,请稍后再试</value>
</data>
<data name="WindowIdentifyMonitorHeader" xml:space="preserve">
<value>显示器编号</value>
</data>
</root>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -6,7 +6,6 @@ using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Web.Hoyolab;
using System.Globalization;
namespace Snap.Hutao.Service;
@@ -16,7 +15,6 @@ internal sealed partial class AppOptions : DbStoreOptions
{
private bool? isEmptyHistoryWishVisible;
private BackdropType? backdropType;
private CultureInfo? currentCulture;
private Region? region;
private string? geetestCustomCompositeUrl;
@@ -26,7 +24,7 @@ internal sealed partial class AppOptions : DbStoreOptions
set => SetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible, value);
}
public List<NameValue<BackdropType>> BackdropTypes { get; } = CollectionsNameValue.FromEnum<BackdropType>();
public List<NameValue<BackdropType>> BackdropTypes { get; } = CollectionsNameValue.FromEnum<BackdropType>(type => type >= 0);
public BackdropType BackdropType
{
@@ -34,14 +32,6 @@ internal sealed partial class AppOptions : DbStoreOptions
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 Region Region
@@ -55,6 +45,4 @@ internal sealed partial class AppOptions : DbStoreOptions
get => GetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl);
set => SetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl, value);
}
internal CultureInfo PreviousCulture { get; set; } = default!;
}

View File

@@ -3,17 +3,11 @@
using Snap.Hutao.Model;
using Snap.Hutao.Web.Hoyolab;
using System.Globalization;
namespace Snap.Hutao.Service;
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)
{
return appOptions.LazyRegions.Value.SingleOrDefault(c => c.Value.Value == appOptions.Region.Value);

View File

@@ -0,0 +1,43 @@
// 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;
}
}
}

View File

@@ -1,24 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model;
using System.Globalization;
using System.IO;
namespace Snap.Hutao.Service.Metadata;
namespace Snap.Hutao.Service;
internal static class MetadataOptionsExtension
internal static class CultureOptionsExtension
{
public static string GetLocalizedLocalFile(this MetadataOptions options, string fileNameWithExtension)
public static NameValue<CultureInfo>? GetCurrentCultureForSelectionOrDefault(this CultureOptions options)
{
return Path.Combine(options.LocalizedDataFolder, fileNameWithExtension);
return options.Cultures.SingleOrDefault(c => c.Value == options.CurrentCulture);
}
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)
public static bool LanguageCodeFitsCurrentLocale(this CultureOptions options, string? languageCode)
{
if (string.IsNullOrEmpty(languageCode))
{
@@ -30,6 +25,11 @@ internal static class MetadataOptionsExtension
return GetLocaleName(cultureInfo) == options.LocaleName;
}
public static string GetLanguageCodeForDocumentationSearch(this CultureOptions options)
{
return LocaleNames.GetLanguageCodeForDocumentationSearchFromLocaleName(options.LocaleName);
}
internal static string GetLocaleName(CultureInfo cultureInfo)
{
while (true)

View File

@@ -58,7 +58,11 @@ internal sealed partial class DailyNoteOptions : DbStoreOptions
{
if (SelectedRefreshTime is not null)
{
scheduleTaskInterop.RegisterForDailyNoteRefresh(SelectedRefreshTime.Value);
if (!scheduleTaskInterop.RegisterForDailyNoteRefresh(SelectedRefreshTime.Value))
{
serviceProvider.GetRequiredService<IInfoBarService>().Warning(SH.ViewModelDailyNoteRegisterTaskFail);
return;
}
}
}
else

View File

@@ -172,6 +172,8 @@ internal static class DiscordController
private static async ValueTask DiscordRunCallbacksAsync(CancellationToken cancellationToken)
{
int notRunningCounter = 0;
using (PeriodicTimer timer = new(TimeSpan.FromMilliseconds(500)))
{
try
@@ -190,7 +192,18 @@ internal static class DiscordController
DiscordResult result = DiscordCoreRunRunCallbacks();
if (result is not DiscordResult.Ok)
{
System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK ERROR]:{result:D} {result}");
if (result is DiscordResult.NotRunning)
{
if (++notRunningCounter > 20)
{
Stop();
}
}
else
{
notRunningCounter = 0;
System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK ERROR]:{result:D} {result}");
}
}
}
catch (SEHException ex)

View File

@@ -11,14 +11,14 @@ internal sealed partial class DiscordService : IDiscordService, IDisposable
{
private readonly RuntimeOptions runtimeOptions;
public async ValueTask SetPlayingActivity(bool isOversea)
public async ValueTask SetPlayingActivityAsync(bool isOversea)
{
_ = isOversea
? await DiscordController.SetPlayingGenshinImpactAsync().ConfigureAwait(false)
: await DiscordController.SetPlayingYuanShenAsync().ConfigureAwait(false);
}
public async ValueTask SetNormalActivity()
public async ValueTask SetNormalActivityAsync()
{
_ = await DiscordController.SetDefaultActivityAsync(runtimeOptions.AppLaunchTime).ConfigureAwait(false);
}

View File

@@ -5,7 +5,7 @@ namespace Snap.Hutao.Service.Discord;
internal interface IDiscordService
{
ValueTask SetNormalActivity();
ValueTask SetNormalActivityAsync();
ValueTask SetPlayingActivity(bool isOversea);
ValueTask SetPlayingActivityAsync(bool isOversea);
}

View File

@@ -226,7 +226,7 @@ internal sealed partial class GachaLogService : IGachaLogService
break;
}
await Delay.Random(1000, 2000).ConfigureAwait(false);
await Delay.RandomMilliSeconds(1000, 2000).ConfigureAwait(false);
}
while (true);
@@ -238,7 +238,7 @@ internal sealed partial class GachaLogService : IGachaLogService
// save items for each queryType
token.ThrowIfCancellationRequested();
fetchContext.SaveItems();
await Delay.Random(1000, 2000).ConfigureAwait(false);
await Delay.RandomMilliSeconds(1000, 2000).ConfigureAwait(false);
}
return new(!fetchContext.FetchStatus.AuthKeyTimeout, fetchContext.TargetArchive);

View File

@@ -18,7 +18,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider;
internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryProvider
{
private readonly IContentDialogFactory contentDialogFactory;
private readonly MetadataOptions metadataOptions;
private readonly CultureOptions cultureOptions;
/// <inheritdoc/>
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")
{
string? queryLanguageCode = query["lang"];
if (metadataOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
if (cultureOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
{
return new(true, new(queryString));
}
else
{
string message = SH.FormatServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale(queryLanguageCode, metadataOptions.LanguageCode);
string message = SH.FormatServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale(queryLanguageCode, cultureOptions.LanguageCode);
return new(false, message);
}
}

View File

@@ -20,7 +20,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider;
internal sealed partial class GachaLogQuerySTokenProvider : IGachaLogQueryProvider
{
private readonly BindingClient2 bindingClient2;
private readonly MetadataOptions metadataOptions;
private readonly CultureOptions cultureOptions;
private readonly IUserService userService;
/// <inheritdoc/>
@@ -38,7 +38,7 @@ internal sealed partial class GachaLogQuerySTokenProvider : IGachaLogQueryProvid
if (authkeyResponse.IsOk())
{
return new(true, new(ComposeQueryString(data, authkeyResponse.Data, metadataOptions.LanguageCode)));
return new(true, new(ComposeQueryString(data, authkeyResponse.Data, cultureOptions.LanguageCode)));
}
else
{

View File

@@ -22,7 +22,7 @@ namespace Snap.Hutao.Service.GachaLog.QueryProvider;
internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProvider
{
private readonly IGameServiceFacade gameService;
private readonly MetadataOptions metadataOptions;
private readonly CultureOptions cultureOptions;
/// <summary>
/// 获取缓存文件路径
@@ -90,12 +90,12 @@ internal sealed partial class GachaLogQueryWebCacheProvider : IGachaLogQueryProv
NameValueCollection query = HttpUtility.ParseQueryString(result.TrimEnd("#/log"));
string? queryLanguageCode = query["lang"];
if (metadataOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
if (cultureOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
{
return new(true, new(result));
}
string message = SH.FormatServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale(queryLanguageCode, metadataOptions.LanguageCode);
string message = SH.FormatServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale(queryLanguageCode, cultureOptions.LanguageCode);
return new(false, message);
}
}

View File

@@ -17,7 +17,7 @@ internal sealed partial class UIGFExportService : IUIGFExportService
{
private readonly IGachaLogDbService gachaLogDbService;
private readonly RuntimeOptions runtimeOptions;
private readonly MetadataOptions metadataOptions;
private readonly CultureOptions cultureOptions;
private readonly ITaskContext taskContext;
/// <inheritdoc/>
@@ -31,7 +31,7 @@ internal sealed partial class UIGFExportService : IUIGFExportService
UIGF uigf = new()
{
Info = UIGFInfo.From(runtimeOptions, metadataOptions, archive.Uid),
Info = UIGFInfo.From(runtimeOptions, cultureOptions, archive.Uid),
List = list,
};

View File

@@ -18,7 +18,7 @@ namespace Snap.Hutao.Service.GachaLog;
internal sealed partial class UIGFImportService : IUIGFImportService
{
private readonly ILogger<UIGFImportService> logger;
private readonly MetadataOptions metadataOptions;
private readonly CultureOptions cultureOptions;
private readonly IGachaLogDbService gachaLogDbService;
private readonly ITaskContext taskContext;
@@ -37,9 +37,9 @@ internal sealed partial class UIGFImportService : IUIGFImportService
// v2.1 only support CHS
if (version is UIGFVersion.Major2Minor2OrLower)
{
if (!metadataOptions.LanguageCodeFitsCurrentLocale(uigf.Info.Language))
if (!cultureOptions.LanguageCodeFitsCurrentLocale(uigf.Info.Language))
{
string message = SH.FormatServiceGachaUIGFImportLanguageNotMatch(uigf.Info.Language, metadataOptions.LanguageCode);
string message = SH.FormatServiceGachaUIGFImportLanguageNotMatch(uigf.Info.Language, cultureOptions.LanguageCode);
ThrowHelper.InvalidOperation(message);
}

View File

@@ -29,10 +29,9 @@ internal readonly struct ChannelOptions
/// </summary>
public readonly bool IsOversea;
/// <summary>
/// 配置文件路径 当不为 null 时则存在文件读写问题
/// </summary>
public readonly string? ConfigFilePath;
public readonly ChannelOptionsErrorKind ErrorKind;
public readonly string? FilePath;
public ChannelOptions(ChannelType channel, SubChannelType subChannel, bool isOversea)
{
@@ -48,15 +47,20 @@ internal readonly struct ChannelOptions
IsOversea = isOversea;
}
private ChannelOptions(bool isOversea, string? configFilePath)
private ChannelOptions(ChannelOptionsErrorKind errorKind, string? filePath)
{
IsOversea = isOversea;
ConfigFilePath = configFilePath;
ErrorKind = errorKind;
FilePath = filePath;
}
public static ChannelOptions FileNotFound(bool isOversea, string configFilePath)
public static ChannelOptions ConfigurationFileNotFound(string filePath)
{
return new(isOversea, configFilePath);
return new(ChannelOptionsErrorKind.ConfigurationFileNotFound, filePath);
}
public static ChannelOptions GamePathNullOrEmpty()
{
return new(ChannelOptionsErrorKind.GamePathNullOrEmpty, string.Empty);
}
/// <inheritdoc/>

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Configuration;
internal enum ChannelOptionsErrorKind
{
None,
ConfigurationFileNotFound,
GamePathNullOrEmpty,
}

View File

@@ -1,11 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO.Ini;
using Snap.Hutao.Service.Game.Scheme;
using System.IO;
using static Snap.Hutao.Service.Game.GameConstants;
namespace Snap.Hutao.Service.Game.Configuration;
@@ -17,84 +15,22 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
public ChannelOptions GetChannelOptions()
{
if (!launchOptions.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath))
if (!launchOptions.TryGetGameFileSystem(out GameFileSystem? gameFileSystem))
{
throw ThrowHelper.InvalidOperation($"Invalid game path: {gamePath}");
return ChannelOptions.GamePathNullOrEmpty();
}
bool isOversea = LaunchScheme.ExecutableIsOversea(Path.GetFileName(gamePath));
bool isOversea = LaunchScheme.ExecutableIsOversea(gameFileSystem.GameFileName);
if (!File.Exists(configPath))
if (!File.Exists(gameFileSystem.GameConfigFilePath))
{
return ChannelOptions.FileNotFound(isOversea, configPath);
return ChannelOptions.ConfigurationFileNotFound(gameFileSystem.GameConfigFilePath);
}
using (FileStream stream = File.OpenRead(configPath))
{
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;
List<IniParameter> parameters = IniSerializer.DeserializeFromFile(gameFileSystem.GameConfigFilePath).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);
}
}
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;
return new(channel, subChannel, isOversea);
}
}

View File

@@ -1,13 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Game.Scheme;
namespace Snap.Hutao.Service.Game.Configuration;
internal interface IGameChannelOptionsService
{
ChannelOptions GetChannelOptions();
bool SetChannelOptions(LaunchScheme scheme);
}

View File

@@ -10,6 +10,7 @@ namespace Snap.Hutao.Service.Game;
internal static class GameConstants
{
public const string ConfigFileName = "config.ini";
public const string PCGameSDKFilePath = @"YuanShen_Data\Plugins\PCGameSDK.dll";
public const string YuanShenFileName = "YuanShen.exe";
public const string YuanShenFileNameUpper = "YUANSHEN.EXE";
public const string GenshinImpactFileName = "GenshinImpact.exe";

View File

@@ -0,0 +1,39 @@
// 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); }
}

View File

@@ -5,10 +5,8 @@ using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.Game.Account;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.Launching.Handler;
using Snap.Hutao.Service.Game.PathAbstraction;
using Snap.Hutao.Service.Game.Process;
using Snap.Hutao.Service.Game.Scheme;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game;
@@ -23,8 +21,6 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
{
private readonly IGameChannelOptionsService gameChannelOptionsService;
private readonly IGameAccountService gameAccountService;
private readonly IGameProcessService gameProcessService;
private readonly IGamePackageService gamePackageService;
private readonly IGamePathService gamePathService;
/// <inheritdoc/>
@@ -45,12 +41,6 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
return gameChannelOptionsService.GetChannelOptions();
}
/// <inheritdoc/>
public bool SetChannelOptions(LaunchScheme scheme)
{
return gameChannelOptionsService.SetChannelOptions(scheme);
}
/// <inheritdoc/>
public ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme)
{
@@ -63,12 +53,6 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
return gameAccountService.DetectCurrentGameAccount(scheme);
}
/// <inheritdoc/>
public bool SetGameAccount(GameAccount account)
{
return gameAccountService.SetGameAccount(account);
}
/// <inheritdoc/>
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
{
@@ -90,18 +74,6 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
/// <inheritdoc/>
public bool IsGameRunning()
{
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);
return LaunchExecutionEnsureGameNotRunningHandler.IsGameRunning(out _);
}
}

View File

@@ -4,8 +4,6 @@
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.Scheme;
using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Game;
@@ -49,8 +47,6 @@ internal interface IGameServiceFacade
/// <returns>是否正在运行</returns>
bool IsGameRunning();
ValueTask LaunchAsync(IProgress<LaunchStatus> progress);
/// <summary>
/// 异步修改游戏账号名称
/// </summary>
@@ -65,27 +61,5 @@ internal interface IGameServiceFacade
/// <returns>任务</returns>
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);
}

View File

@@ -218,8 +218,7 @@ internal sealed class LaunchOptions : DbStoreOptions
{
if (SetProperty(ref selectedAspectRatio, value) && value is AspectRatio aspectRatio)
{
ScreenWidth = (int)aspectRatio.Width;
ScreenHeight = (int)aspectRatio.Height;
(ScreenWidth, ScreenHeight) = ((int)aspectRatio.Width, (int)aspectRatio.Height);
}
}
}

View File

@@ -3,70 +3,25 @@
using Snap.Hutao.Service.Game.PathAbstraction;
using System.Collections.Immutable;
using System.IO;
namespace Snap.Hutao.Service.Game;
internal static class LaunchOptionsExtension
{
public static bool TryGetGamePathAndGameDirectory(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameDirectory)
{
gamePath = options.GamePath;
gameDirectory = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameDirectory))
{
return false;
}
return true;
}
public static bool TryGetGameDirectoryAndGameFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameDirectory, [NotNullWhen(true)] out string? gameFileName)
public static bool TryGetGameFileSystem(this LaunchOptions options, [NotNullWhen(true)] out GameFileSystem? fileSystem)
{
string gamePath = options.GamePath;
gameDirectory = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameDirectory))
{
gameFileName = default;
return false;
}
gameFileName = Path.GetFileName(gamePath);
if (string.IsNullOrEmpty(gameFileName))
if (string.IsNullOrEmpty(gamePath))
{
fileSystem = default;
return false;
}
fileSystem = new GameFileSystem(gamePath);
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)
{
string gamePath = options.GamePath;

View File

@@ -0,0 +1,38 @@
// 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);
}
}

View File

@@ -0,0 +1,160 @@
// 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;
}
}
}

View File

@@ -0,0 +1,20 @@
// 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);
}
}

View File

@@ -0,0 +1,20 @@
// 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);
}
}

View File

@@ -0,0 +1,58 @@
// 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,
},
};
}
}

View File

@@ -0,0 +1,25 @@
// 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);
}
}

View File

@@ -0,0 +1,72 @@
// 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);
}
}

View File

@@ -0,0 +1,39 @@
// 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);
}
}
}
}

View File

@@ -0,0 +1,26 @@
// 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);
}
}

View File

@@ -0,0 +1,20 @@
// 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);
}
}

View File

@@ -0,0 +1,31 @@
// 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);
}
}
}

View File

@@ -0,0 +1,21 @@
// 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!);
}
}

View File

@@ -0,0 +1,42 @@
// 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);
}
}

View File

@@ -0,0 +1,11 @@
// 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);
}

View File

@@ -0,0 +1,82 @@
// 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;
}
}

View File

@@ -0,0 +1,50 @@
// 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;
}
}

View File

@@ -0,0 +1,11 @@
// 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!;
}

View File

@@ -0,0 +1,20 @@
// 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,
}

View File

@@ -1,102 +0,0 @@
// 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;
}
}
}

View File

@@ -1,11 +0,0 @@
// 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);
}

View File

@@ -8,25 +8,15 @@ namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 包更新状态
/// </summary>
internal sealed class PackageReplaceStatus
internal sealed class PackageConvertStatus
{
/// <summary>
/// 构造一个新的包更新状态
/// </summary>
/// <param name="name">描述</param>
public PackageReplaceStatus(string name)
public PackageConvertStatus(string name)
{
Name = name;
Description = name;
}
/// <summary>
/// 构造一个新的包更新状态
/// </summary>
/// <param name="name">名称</param>
/// <param name="bytesRead">读取的字节数</param>
/// <param name="totalBytes">总字节数</param>
public PackageReplaceStatus(string name, long bytesRead, long totalBytes)
public PackageConvertStatus(string name, long bytesRead, long totalBytes)
{
Percent = (double)bytesRead / totalBytes;
Name = name;

View File

@@ -34,7 +34,7 @@ internal sealed partial class PackageConverter
private readonly HttpClient httpClient;
private readonly ILogger<PackageConverter> logger;
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResource, string gameFolder, IProgress<PackageReplaceStatus> progress)
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResource, string gameFolder, IProgress<PackageConvertStatus> progress)
{
// 以 国服 => 国际 为例
// 1. 下载国际服的 pkg_version 文件,转换为索引字典
@@ -93,7 +93,6 @@ internal sealed partial class PackageConverter
ZipFile.ExtractToDirectory(sdkWebStream, gameFolder, true);
}
// TODO: verify sdk md5
if (File.Exists(sdkDllBackup) && File.Exists(sdkVersionBackup))
{
File.Delete(sdkDllBackup);
@@ -188,7 +187,7 @@ internal sealed partial class PackageConverter
}
}
private async ValueTask PrepareCacheFilesAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
private async ValueTask PrepareCacheFilesAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageConvertStatus> progress)
{
foreach (PackageItemOperationInfo info in operations)
{
@@ -204,7 +203,7 @@ internal sealed partial class PackageConverter
}
}
private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress<PackageConvertStatus> progress)
{
// 还原正确的远程地址
string remoteName = string.Format(CultureInfo.CurrentCulture, info.Remote.RelativePath, context.ToDataFolderName);
@@ -230,7 +229,7 @@ internal sealed partial class PackageConverter
Directory.CreateDirectory(directory);
string remoteUrl = context.GetScatteredFilesUrl(remoteName);
HttpShardCopyWorkerOptions<PackageReplaceStatus> options = new()
HttpShardCopyWorkerOptions<PackageConvertStatus> options = new()
{
HttpClient = httpClient,
SourceUrl = remoteUrl,
@@ -238,7 +237,7 @@ internal sealed partial class PackageConverter
StatusFactory = (bytesRead, totalBytes) => new(remoteName, bytesRead, totalBytes),
};
using (HttpShardCopyWorker<PackageReplaceStatus> worker = await HttpShardCopyWorker<PackageReplaceStatus>.CreateAsync(options).ConfigureAwait(false))
using (HttpShardCopyWorker<PackageConvertStatus> worker = await HttpShardCopyWorker<PackageConvertStatus>.CreateAsync(options).ConfigureAwait(false))
{
try
{
@@ -258,7 +257,7 @@ internal sealed partial class PackageConverter
}
}
private async ValueTask<bool> ReplaceGameResourceAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
private async ValueTask<bool> ReplaceGameResourceAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageConvertStatus> progress)
{
// 执行下载与移动操作
foreach (PackageItemOperationInfo info in operations)

View File

@@ -9,7 +9,7 @@ internal sealed class GamePathEntry
public string Path { get; set; } = default!;
[JsonIgnore]
public GamePathKind Kind { get => GetKind(Path); }
public GamePathEntryKind Kind { get => GetKind(Path); }
public static GamePathEntry Create(string path)
{
@@ -19,8 +19,8 @@ internal sealed class GamePathEntry
};
}
private static GamePathKind GetKind(string path)
private static GamePathEntryKind GetKind(string path)
{
return GamePathKind.None;
return GamePathEntryKind.None;
}
}

View File

@@ -3,7 +3,7 @@
namespace Snap.Hutao.Service.Game.PathAbstraction;
internal enum GamePathKind
internal enum GamePathEntryKind
{
None,
ChineseClient,

View File

@@ -1,186 +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.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;
}
}
}

View File

@@ -1,11 +0,0 @@
// 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);
}

View File

@@ -1,19 +0,0 @@
// 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);
}
}
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Metadata;
namespace Snap.Hutao.Service;
/// <summary>
/// 本地化名称
@@ -74,4 +74,15 @@ internal static class LocaleNames
return !string.IsNullOrEmpty(languageCode);
}
public static string GetLanguageCodeForDocumentationSearchFromLocaleName(string localeName)
{
return localeName switch
{
ID => "id-id",
RU => "ru-ru",
CHS => "zh-cn",
_ => "en-us",
};
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core;
using System.Globalization;
using System.IO;
namespace Snap.Hutao.Service.Metadata;
@@ -10,10 +11,9 @@ namespace Snap.Hutao.Service.Metadata;
[Injection(InjectAs.Singleton)]
internal sealed partial class MetadataOptions
{
private readonly AppOptions appOptions;
private readonly RuntimeOptions hutaoOptions;
private readonly CultureOptions cultureOptions;
private readonly RuntimeOptions runtimeOptions;
private string? localeName;
private string? fallbackDataFolder;
private string? localizedDataFolder;
@@ -23,7 +23,7 @@ internal sealed partial class MetadataOptions
{
if (fallbackDataFolder is null)
{
fallbackDataFolder = Path.Combine(hutaoOptions.DataFolder, "Metadata", "CHS");
fallbackDataFolder = Path.Combine(runtimeOptions.DataFolder, "Metadata", LocaleNames.CHS);
Directory.CreateDirectory(fallbackDataFolder);
}
@@ -37,7 +37,7 @@ internal sealed partial class MetadataOptions
{
if (localizedDataFolder is null)
{
localizedDataFolder = Path.Combine(hutaoOptions.DataFolder, "Metadata", LocaleName);
localizedDataFolder = Path.Combine(runtimeOptions.DataFolder, "Metadata", cultureOptions.LocaleName);
Directory.CreateDirectory(localizedDataFolder);
}
@@ -45,21 +45,13 @@ internal sealed partial class MetadataOptions
}
}
public string LocaleName
public string GetLocalizedLocalFile(string fileNameWithExtension)
{
get => localeName ??= MetadataOptionsExtension.GetLocaleName(appOptions.CurrentCulture);
return Path.Combine(LocalizedDataFolder, fileNameWithExtension);
}
public string LanguageCode
public string GetLocalizedRemoteFile(string fileNameWithExtension)
{
get
{
if (LocaleNames.TryGetLanguageCodeFromLocaleName(LocaleName, out string? languageCode))
{
return languageCode;
}
throw new KeyNotFoundException($"Invalid localeName: '{LocaleName}'");
}
return Web.HutaoEndpoints.Metadata(cultureOptions.LocaleName, fileNameWithExtension);
}
}

View File

@@ -1,44 +0,0 @@
// 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;
}
}

View File

@@ -1,9 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Navigation;
internal interface IDocumentationProvider
{
string GetDocumentation();
}

View File

@@ -107,6 +107,7 @@
<None Remove="Control\Theme\Uri.xaml" />
<None Remove="Control\Theme\WindowOverride.xaml" />
<None Remove="GuideWindow.xaml" />
<None Remove="IdentifyMonitorWindow.xaml" />
<None Remove="IdentityStructs.json" />
<None Remove="LaunchGameWindow.xaml" />
<None Remove="Resource\BlurBackground.png" />
@@ -127,6 +128,7 @@
<None Remove="Resource\Navigation\DailyNote.png" />
<None Remove="Resource\Navigation\Database.png" />
<None Remove="Resource\Navigation\Documentation.png" />
<None Remove="Resource\Navigation\Feedback.png" />
<None Remove="Resource\Navigation\GachaLog.png" />
<None Remove="Resource\Navigation\LaunchGame.png" />
<None Remove="Resource\Navigation\SpiralAbyss.png" />
@@ -185,6 +187,7 @@
<None Remove="View\Page\AvatarPropertyPage.xaml" />
<None Remove="View\Page\CultivationPage.xaml" />
<None Remove="View\Page\DailyNotePage.xaml" />
<None Remove="View\Page\FeedbackPage.xaml" />
<None Remove="View\Page\GachaLogPage.xaml" />
<None Remove="View\Page\LaunchGamePage.xaml" />
<None Remove="View\Page\LoginMihoyoUserPage.xaml" />
@@ -268,6 +271,7 @@
<Content Include="Resource\Navigation\DailyNote.png" />
<Content Include="Resource\Navigation\Database.png" />
<Content Include="Resource\Navigation\Documentation.png" />
<Content Include="Resource\Navigation\Feedback.png" />
<Content Include="Resource\Navigation\GachaLog.png" />
<Content Include="Resource\Navigation\LaunchGame.png" />
<Content Include="Resource\Navigation\SpiralAbyss.png" />
@@ -321,7 +325,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.507">
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -344,6 +348,11 @@
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<Page Update="View\Page\FeedbackPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Dialog\ReconfirmDialog.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -544,7 +553,13 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="IdentifyMonitorWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Control\HutaoStatisticsCard.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -40,6 +40,7 @@
</Grid.RowDefinitions>
<FontIcon
Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="{ThemeResource TitleTextBlockFontSize}"
@@ -61,16 +62,22 @@
Content="{StaticResource FontIconContentSetting}"
FontFamily="{StaticResource SymbolThemeFontFamily}"
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageHomeLaunchGameSettingAction}"/>
<shc:SizeRestrictedContentControl
<StackPanel
Grid.Row="2"
Grid.ColumnSpan="3"
VerticalAlignment="Bottom">
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding GameAccountsView}"
PlaceholderText="{shcm:ResourceString Name=ViewCardLaunchGameSelectAccountPlaceholder}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
VerticalAlignment="Bottom"
Spacing="8">
<TextBlock
Opacity="0.7"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding LaunchStatusOptions.LaunchStatus.Description, Mode=OneWay}"/>
<shc:SizeRestrictedContentControl VerticalAlignment="Bottom">
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding GameAccountsView}"
PlaceholderText="{shcm:ResourceString Name=ViewCardLaunchGameSelectAccountPlaceholder}"
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
</StackPanel>
</Grid>
</Grid>
</Button>

View File

@@ -11,7 +11,7 @@ namespace Snap.Hutao.View.Dialog;
/// 启动游戏客户端转换对话框
/// </summary>
[HighQuality]
[DependencyProperty("State", typeof(PackageReplaceStatus))]
[DependencyProperty("State", typeof(PackageConvertStatus))]
internal sealed partial class LaunchGamePackageConvertDialog : ContentDialog
{
/// <summary>

View File

@@ -55,7 +55,7 @@
<GridView
Grid.Row="0"
ItemTemplate="{StaticResource LanguageTemplate}"
ItemsSource="{Binding AppOptions.Cultures}"
ItemsSource="{Binding CultureOptions.Cultures}"
SelectedItem="{Binding SelectedCulture, Mode=TwoWay}"
SelectionMode="Single"/>
</Grid>

View File

@@ -27,6 +27,10 @@
shvh:NavHelper.NavigateTo="shvp:AnnouncementPage"
Content="{shcm:ResourceString Name=ViewAnnouncementHeader}"
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}"/>

View File

@@ -0,0 +1,257 @@
<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=&#xECAA;}"
IsExpanded="True">
<cwc:SettingsExpander.Items>
<cwc:SettingsCard
ActionIcon="{shcm:FontIcon Glyph=&#xE8C8;}"
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=&#xE71B;}"
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=&#xF8A5;}"
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=&#xE713;}"
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=&#xE721;}"
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>

View File

@@ -0,0 +1,16 @@
// 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();
}
}

View File

@@ -171,8 +171,8 @@
Text="{shcm:ResourceString Name=ViewPageLaunchGameSwitchSchemeWarning}"/>
</StackPanel>
</cwc:SettingsCard.Description>
<StackPanel Orientation="Horizontal">
<shvc:Elevation Margin="0,0,36,0" Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<StackPanel Orientation="Horizontal" Spacing="{ThemeResource SettingsCardContentControlSpacing}">
<shvc:Elevation Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<shc:SizeRestrictedContentControl>
<shccs:ComboBox2
DisplayMemberPath="DisplayName"
@@ -203,7 +203,7 @@
Description="{shcm:ResourceString Name=ViewPageLaunchGameWindowsHDRDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameWindowsHDRHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE7F7;}">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsWindowsHDREnabled, Mode=TwoWay}"/>
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsWindowsHDREnabled, Mode=TwoWay}"/>
</cwc:SettingsCard>
<!-- 进程 -->
@@ -213,81 +213,88 @@
Description="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameArgumentsHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE943;}">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsEnabled, Mode=TwoWay}"/>
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsEnabled, Mode=TwoWay}"/>
<cwc:SettingsExpander.Items>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceExclusiveDescription}" Header="-window-mode exclusive">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsExclusive, Mode=TwoWay}"/>
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsExclusive, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceFullscreenDescription}" Header="-screen-fullscreen">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsFullScreen, Mode=TwoWay}"/>
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsFullScreen, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceBorderlessDescription}" Header="-popupwindow">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsBorderless, Mode=TwoWay}"/>
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsBorderless, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceCloudThirdPartyMobileDescription}" Header="-platform_type CLOUD_THIRD_PARTY_MOBILE">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsUseCloudThirdPartyMobile, Mode=TwoWay}"/>
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsUseCloudThirdPartyMobile, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioDescription}" Header="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioHeader}">
<shc:SizeRestrictedContentControl Margin="0,0,136,0">
<shc:SizeRestrictedContentControl Margin="0,0,130,0" VerticalAlignment="Center">
<ComboBox
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
ItemsSource="{Binding LaunchOptions.AspectRatios}"
PlaceholderText="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceAspectRatioPlaceHolder}"
SelectedItem="{Binding LaunchOptions.SelectedAspectRatio, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenWidthDescription}" Header="-screen-width">
<StackPanel Orientation="Horizontal" Spacing="16">
<StackPanel Orientation="Horizontal" Spacing="10">
<NumberBox
Width="156"
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
Padding="12,6,0,0"
VerticalAlignment="Center"
IsEnabled="{Binding LaunchOptions.IsScreenWidthEnabled}"
Value="{Binding LaunchOptions.ScreenWidth, Mode=TwoWay}"/>
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsScreenWidthEnabled, Mode=TwoWay}"/>
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsScreenWidthEnabled, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameAppearanceScreenHeightDescription}" Header="-screen-height">
<StackPanel Orientation="Horizontal" Spacing="16">
<StackPanel Orientation="Horizontal" Spacing="10">
<NumberBox
Width="156"
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
Padding="12,6,0,0"
VerticalAlignment="Center"
IsEnabled="{Binding LaunchOptions.IsScreenHeightEnabled}"
Value="{Binding LaunchOptions.ScreenHeight, Mode=TwoWay}"/>
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsScreenHeightEnabled, Mode=TwoWay}"/>
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsScreenHeightEnabled, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsCard>
<cwc:SettingsCard Description="{shcm:ResourceString Name=ViewPageLaunchGameMonitorsDescription}" Header="-monitor">
<StackPanel Orientation="Horizontal" Spacing="16">
<shc:SizeRestrictedContentControl>
<StackPanel Orientation="Horizontal" Spacing="10">
<Button
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
Command="{Binding IdentifyMonitorsCommand}"
Content="{shcm:ResourceString Name=ViewModelLaunchGameIdentifyMonitorsAction}"/>
<shc:SizeRestrictedContentControl VerticalAlignment="Center">
<ComboBox
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
DisplayMemberPath="Name"
IsEnabled="{Binding LaunchOptions.IsMonitorEnabled}"
ItemsSource="{Binding LaunchOptions.Monitors}"
SelectedItem="{Binding LaunchOptions.Monitor, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.IsMonitorEnabled, Mode=TwoWay}"/>
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.IsMonitorEnabled, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsCard>
</cwc:SettingsExpander.Items>
</cwc:SettingsExpander>
<cwc:SettingsCard
Padding="{ThemeResource SettingsCardAlignSettingsExpanderPadding}"
Description="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE785;}"
IsEnabled="{Binding RuntimeOptions.IsElevated}"
Visibility="{Binding LaunchOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Orientation="Horizontal">
<shvc:Elevation Margin="0,0,36,0" Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<StackPanel Orientation="Horizontal" Spacing="10">
<shvc:Elevation Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<NumberBox
MinWidth="156"
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
Padding="10,8,0,0"
Maximum="720"
Minimum="60"
SpinButtonPlacementMode="Inline"
Value="{Binding LaunchOptions.TargetFps, Mode=TwoWay}"/>
<ToggleSwitch
Width="120"
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
IsOn="{Binding LaunchOptions.UnlockFps, Mode=TwoWay}"
OffContent="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsOff}"
OnContent="{shcm:ResourceString Name=ViewPageLaunchGameUnlockFpsOn}"/>
@@ -300,13 +307,13 @@
Description="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGamePlayTimeHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xEC92;}">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.UseStarwardPlayTimeStatistics, Mode=TwoWay}"/>
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.UseStarwardPlayTimeStatistics, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityDescription}"
Header="{shcm:ResourceString Name=ViewPageLaunchGameDiscordActivityHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE8CF;}">
<ToggleSwitch Width="120" IsOn="{Binding LaunchOptions.SetDiscordActivityWhenPlaying, Mode=TwoWay}"/>
<ToggleSwitch MinWidth="{ThemeResource SettingsCardContentControlMinWidth}" IsOn="{Binding LaunchOptions.SetDiscordActivityWhenPlaying, Mode=TwoWay}"/>
</cwc:SettingsCard>
</StackPanel>
</Grid>
@@ -355,8 +362,8 @@
VerticalAlignment="Center"
Spacing="3">
<shci:CachedImage
Width="120"
Height="120"
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
EnableLazyLoading="False"
Source="{StaticResource UI_EmotionIcon445}"/>
<TextBlock

View File

@@ -97,7 +97,7 @@
Description="{shcm:ResourceString Name=ViewPageSettingElevatedModeDescription}"
Header="{shcm:ResourceString Name=ViewPageSettingElevatedModeRestartAction}"
IsClickEnabled="True"
IsEnabled="{Binding HutaoOptions.IsElevated, Converter={StaticResource BoolNegationConverter}}"/>
IsEnabled="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolNegationConverter}}"/>
<cwc:SettingsCard
ActionIconToolTip="{shcm:ResourceString Name=ViewPageSettingCreateDesktopShortcutAction}"
Command="{Binding CreateDesktopShortcutCommand}"
@@ -111,23 +111,6 @@
</shch:ScrollViewerHelper.RightPanel>
<Grid Padding="16" HorizontalAlignment="Left">
<StackPanel Grid.Column="0" Spacing="{StaticResource SettingsCardSpacing}">
<cwc:SettingsExpander
Description="{Binding HutaoOptions.Version}"
Header="{shcm:ResourceString Name=AppName}"
HeaderIcon="{shcm:FontIcon Glyph=&#xECAA;}"
IsExpanded="True">
<cwc:SettingsExpander.Items>
<cwc:SettingsCard
ActionIcon="{shcm:FontIcon Glyph=&#xE8C8;}"
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
ItemsRepeater will behave abnormal if no direct scrollhost wrapping it
@@ -213,7 +196,7 @@
<shc:SizeRestrictedContentControl>
<ComboBox
DisplayMemberPath="Name"
ItemsSource="{Binding AppOptions.Cultures}"
ItemsSource="{Binding CultureOptions.Cultures}"
SelectedItem="{Binding SelectedCulture, Mode=TwoWay}"/>
</shc:SizeRestrictedContentControl>
</cwc:SettingsCard>
@@ -416,7 +399,7 @@
<cwc:SettingsCard
Header="{shcm:ResourceString Name=ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE730;}"
IsEnabled="{Binding HutaoOptions.IsElevated}">
IsEnabled="{Binding RuntimeOptions.IsElevated}">
<cwc:SettingsCard.Description>
<StackPanel>
<TextBlock
@@ -427,7 +410,7 @@
</StackPanel>
</cwc:SettingsCard.Description>
<StackPanel Orientation="Horizontal">
<shvc:Elevation Visibility="{Binding HutaoOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<shvc:Elevation Visibility="{Binding RuntimeOptions.IsElevated, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<ToggleSwitch Width="120" IsOn="{Binding IsAdvancedLaunchOptionsEnabled, Mode=TwoWay}"/>
</StackPanel>
</cwc:SettingsCard>

View File

@@ -61,31 +61,6 @@
</ResourceDictionary>
</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.Content>
<Grid>

View File

@@ -0,0 +1,112 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Core.IO.DataTransfer;
using Snap.Hutao.Service;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Web.Hutao;
using Snap.Hutao.Web.Hutao.Algolia;
using Snap.Hutao.Web.Response;
using System.Runtime.InteropServices;
using Windows.System;
namespace Snap.Hutao.ViewModel.Feedback;
[Injection(InjectAs.Scoped)]
[ConstructorGenerated]
internal sealed partial class FeedbackViewModel : Abstraction.ViewModel
{
private readonly HutaoInfrastructureClient hutaoInfrastructureClient;
private readonly HutaoDocumentationClient hutaoDocumentationClient;
private readonly IClipboardProvider clipboardProvider;
private readonly IInfoBarService infoBarService;
private readonly CultureOptions cultureOptions;
private readonly RuntimeOptions runtimeOptions;
private readonly ITaskContext taskContext;
private string? searchText;
private List<AlgoliaHit>? searchResults;
private IPInformation? ipInformation;
public RuntimeOptions RuntimeOptions { get => runtimeOptions; }
public string? SearchText { get => searchText; set => SetProperty(ref searchText, value); }
public List<AlgoliaHit>? SearchResults { get => searchResults; set => SetProperty(ref searchResults, value); }
public IPInformation? IPInformation { get => ipInformation; private set => SetProperty(ref ipInformation, value); }
protected override async ValueTask<bool> InitializeUIAsync()
{
Response<IPInformation> resp = await hutaoInfrastructureClient.GetIPInformationAsync().ConfigureAwait(false);
IPInformation info;
if (resp.IsOk())
{
info = resp.Data;
}
else
{
info = IPInformation.Default;
}
await taskContext.SwitchToMainThreadAsync();
IPInformation = info;
return true;
}
[Command("NavigateToUriCommand")]
private static async Task NavigateToUri(string? uri)
{
if (string.IsNullOrEmpty(uri))
{
return;
}
await Launcher.LaunchUriAsync(uri.ToUri());
}
[Command("SearchDocumentCommand")]
private async Task SearchDocumentAsync(string? searchText)
{
IsInitialized = false;
SearchResults = null;
if (string.IsNullOrEmpty(searchText))
{
IsInitialized = true;
return;
}
string language = cultureOptions.GetLanguageCodeForDocumentationSearch();
AlgoliaResponse? response = await hutaoDocumentationClient.QueryAsync(searchText, language).ConfigureAwait(false);
await taskContext.SwitchToMainThreadAsync();
if (response is { Results: [AlgoliaResult { Hits: { Count: > 0 } hits }, ..] })
{
SearchResults = [.. hits.DistinctBy(hit => hit.Url)];
}
else
{
SearchResults = null;
}
IsInitialized = true;
}
[Command("CopyDeviceIdCommand")]
private void CopyDeviceId()
{
try
{
clipboardProvider.SetText(RuntimeOptions.DeviceId);
infoBarService.Success(SH.ViewModelSettingCopyDeviceIdSuccess);
}
catch (COMException ex)
{
infoBarService.Error(ex);
}
}
}

Some files were not shown because too many files have changed in this diff Show More