diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Primitive/SchemeType.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Primitive/SchemeType.cs
index 12971805..80696756 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/Primitive/SchemeType.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/Primitive/SchemeType.cs
@@ -12,7 +12,7 @@ internal enum SchemeType
///
/// 国际服
///
- Mihoyo,
+ Hoyoverse,
///
/// 国服官服
diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs
index 97474eb7..28a54a49 100644
--- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/SettingEntry.Constant.cs
@@ -13,6 +13,8 @@ internal sealed partial class SettingEntry
///
public const string GamePath = "GamePath";
+ public const string GamePathEntries = "GamePathEntries";
+
///
/// PowerShell 路径
///
diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
index ab31fad0..1c7f44cb 100644
--- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
+++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx
@@ -2189,6 +2189,9 @@
打开截图文件夹
+
+ 选择游戏路径
+
关于 胡桃
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs
index e02352d9..8c9adcfa 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptions.cs
@@ -6,6 +6,8 @@ using Snap.Hutao.Core.Windowing;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
+using Snap.Hutao.Service.Game.PathAbstraction;
+using System.Collections.Immutable;
using System.Globalization;
using System.IO;
@@ -15,20 +17,12 @@ namespace Snap.Hutao.Service;
[Injection(InjectAs.Singleton)]
internal sealed partial class AppOptions : DbStoreOptions
{
- private string? gamePath;
private string? powerShellPath;
private bool? isEmptyHistoryWishVisible;
private BackdropType? backdropType;
private CultureInfo? currentCulture;
- private bool? isAdvancedLaunchOptionsEnabled;
private string? geetestCustomCompositeUrl;
- public string GamePath
- {
- get => GetOption(ref gamePath, SettingEntry.GamePath);
- set => SetOption(ref gamePath, SettingEntry.GamePath, value);
- }
-
public string PowerShellPath
{
get
@@ -80,14 +74,6 @@ internal sealed partial class AppOptions : DbStoreOptions
set => SetOption(ref currentCulture, SettingEntry.Culture, value, value => value.Name);
}
- public bool IsAdvancedLaunchOptionsEnabled
- {
- // DO NOT MOVE TO OTHER CLASS
- // We use this property in SettingPage binding
- get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled);
- set => SetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled, value);
- }
-
public string GeetestCustomCompositeUrl
{
get => GetOption(ref geetestCustomCompositeUrl, SettingEntry.GeetestCustomCompositeUrl);
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs
index 0359ed1d..34c731f1 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/AppOptionsExtension.cs
@@ -2,6 +2,8 @@
// Licensed under the MIT license.
using Snap.Hutao.Model;
+using Snap.Hutao.Service.Game.PathAbstraction;
+using System.Collections.Immutable;
using System.Globalization;
using System.IO;
@@ -9,39 +11,6 @@ namespace Snap.Hutao.Service;
internal static class AppOptionsExtension
{
- public static bool TryGetGameFolderAndFileName(this AppOptions appOptions, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName)
- {
- string gamePath = appOptions.GamePath;
-
- gameFolder = Path.GetDirectoryName(gamePath);
- if (string.IsNullOrEmpty(gameFolder))
- {
- gameFileName = default;
- return false;
- }
-
- gameFileName = Path.GetFileName(gamePath);
- if (string.IsNullOrEmpty(gameFileName))
- {
- return false;
- }
-
- return true;
- }
-
- public static bool TryGetGamePathAndGameFileName(this AppOptions appOptions, out string gamePath, [NotNullWhen(true)] out string? gameFileName)
- {
- gamePath = appOptions.GamePath;
-
- gameFileName = Path.GetFileName(gamePath);
- if (string.IsNullOrEmpty(gameFileName))
- {
- return false;
- }
-
- return true;
- }
-
public static NameValue? GetCurrentCultureForSelectionOrDefault(this AppOptions appOptions)
{
return appOptions.Cultures.SingleOrDefault(c => c.Value == appOptions.CurrentCulture);
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs
index 80fd68d3..2b63c89a 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs
@@ -13,11 +13,11 @@ namespace Snap.Hutao.Service.Game.Configuration;
[Injection(InjectAs.Singleton, typeof(IGameChannelOptionsService))]
internal sealed partial class GameChannelOptionsService : IGameChannelOptionsService
{
- private readonly AppOptions appOptions;
+ private readonly LaunchOptions launchOptions;
public ChannelOptions GetChannelOptions()
{
- string gamePath = appOptions.GamePath;
+ string gamePath = launchOptions.GamePath;
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName);
bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase);
@@ -38,7 +38,7 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
public bool SetChannelOptions(LaunchScheme scheme)
{
- string gamePath = appOptions.GamePath;
+ string gamePath = launchOptions.GamePath;
string? directory = Path.GetDirectoryName(gamePath);
ArgumentException.ThrowIfNullOrEmpty(directory);
string configPath = Path.Combine(directory, ConfigFileName);
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs
index cb20776b..115d1ee7 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs
@@ -5,6 +5,7 @@ using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.Account;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package;
+using Snap.Hutao.Service.Game.PathAbstraction;
using Snap.Hutao.Service.Game.Process;
using Snap.Hutao.Service.Game.Scheme;
using System.Collections.ObjectModel;
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs
index 4241bba3..b4cbe11a 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptions.cs
@@ -6,6 +6,8 @@ using Microsoft.UI.Windowing;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
+using Snap.Hutao.Service.Game.PathAbstraction;
+using System.Collections.Immutable;
using System.Globalization;
using Windows.Graphics;
using Windows.Win32.Foundation;
@@ -24,7 +26,10 @@ internal sealed class LaunchOptions : DbStoreOptions
private readonly int primaryScreenHeight;
private readonly int primaryScreenFps;
+ private string? gamePath;
+ private ImmutableList? gamePathEntries;
private bool? isEnabled;
+ private bool? isAdvancedLaunchOptionsEnabled;
private bool? isFullScreen;
private bool? isBorderless;
private bool? isExclusive;
@@ -85,12 +90,32 @@ internal sealed class LaunchOptions : DbStoreOptions
}
}
+ public string GamePath
+ {
+ get => GetOption(ref gamePath, SettingEntry.GamePath);
+ set => SetOption(ref gamePath, SettingEntry.GamePath, value);
+ }
+
+ public ImmutableList GamePathEntries
+ {
+ // Because DbStoreOptions can't detect collection change, We use
+ // ImmutableList to imply that the whole list needs to be replaced
+ get => GetOption(ref gamePathEntries, SettingEntry.GamePathEntries, raw => JsonSerializer.Deserialize>(raw), []);
+ set => SetOption(ref gamePathEntries, SettingEntry.GamePathEntries, value, value => JsonSerializer.Serialize(value));
+ }
+
public bool IsEnabled
{
get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, true);
set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value);
}
+ public bool IsAdvancedLaunchOptionsEnabled
+ {
+ get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled);
+ set => SetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled, value);
+ }
+
public bool IsFullScreen
{
get => GetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen);
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs
new file mode 100644
index 00000000..5913e00e
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs
@@ -0,0 +1,87 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+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 TryGetGameFolderAndFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName)
+ {
+ string gamePath = options.GamePath;
+
+ gameFolder = Path.GetDirectoryName(gamePath);
+ if (string.IsNullOrEmpty(gameFolder))
+ {
+ gameFileName = default;
+ return false;
+ }
+
+ gameFileName = Path.GetFileName(gamePath);
+ if (string.IsNullOrEmpty(gameFileName))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static bool TryGetGamePathAndGameFileName(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameFileName)
+ {
+ gamePath = options.GamePath;
+
+ gameFileName = Path.GetFileName(gamePath);
+ if (string.IsNullOrEmpty(gameFileName))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static ImmutableList GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry)
+ {
+ string gamePath = options.GamePath;
+
+ if (string.IsNullOrEmpty(gamePath))
+ {
+ entry = default;
+ return options.GamePathEntries;
+ }
+
+ if (options.GamePathEntries.SingleOrDefault(entry => string.Equals(entry.Path, options.GamePath, StringComparison.OrdinalIgnoreCase)) is { } existed)
+ {
+ entry = existed;
+ return options.GamePathEntries;
+ }
+
+ entry = GamePathEntry.Create(options.GamePath);
+ return [.. options.GamePathEntries, entry];
+ }
+
+ public static ImmutableList RemoveGamePathEntry(this LaunchOptions options, GamePathEntry? entry, out GamePathEntry? selected)
+ {
+ if (entry is not null)
+ {
+ if (string.Equals(options.GamePath, entry.Path, StringComparison.OrdinalIgnoreCase))
+ {
+ options.GamePath = string.Empty;
+ }
+
+ options.GamePathEntries = options.GamePathEntries.Remove(entry);
+ }
+
+ return options.GetGamePathEntries(out selected);
+ }
+
+ public static ImmutableList UpdateGamePathAndRefreshEntries(this LaunchOptions options, string gamePath)
+ {
+ options.GamePath = gamePath;
+ ImmutableList entries = options.GetGamePathEntries(out _);
+ options.GamePathEntries = entries;
+ return entries;
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs
index f230c984..09be982f 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs
@@ -16,12 +16,12 @@ internal sealed partial class GamePackageService : IGamePackageService
{
private readonly PackageConverter packageConverter;
private readonly IServiceProvider serviceProvider;
+ private readonly LaunchOptions launchOptions;
private readonly ITaskContext taskContext;
- private readonly AppOptions appOptions;
public async ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress)
{
- if (!appOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName))
+ if (!launchOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName))
{
return false;
}
@@ -58,7 +58,7 @@ internal sealed partial class GamePackageService : IGamePackageService
string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName;
await taskContext.SwitchToMainThreadAsync();
- appOptions.GamePath = Path.Combine(gameFolder, exeName);
+ launchOptions.UpdateGamePathAndRefreshEntries(Path.Combine(gameFolder, exeName));
}
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false);
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathEntry.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathEntry.cs
new file mode 100644
index 00000000..8d8dfd2e
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathEntry.cs
@@ -0,0 +1,26 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.Game.PathAbstraction;
+
+internal sealed class GamePathEntry
+{
+ [JsonPropertyName("Path")]
+ public string Path { get; set; } = default!;
+
+ [JsonIgnore]
+ public GamePathKind Kind { get => GetKind(Path); }
+
+ public static GamePathEntry Create(string path)
+ {
+ return new()
+ {
+ Path = path,
+ };
+ }
+
+ private static GamePathKind GetKind(string path)
+ {
+ return GamePathKind.None;
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathKind.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathKind.cs
new file mode 100644
index 00000000..3271c606
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathKind.cs
@@ -0,0 +1,13 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Service.Game.PathAbstraction;
+
+internal enum GamePathKind
+{
+ None,
+ ChineseClient,
+ OverseaClient,
+ ChineseCloud,
+ OverseaCloud,
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs
similarity index 78%
rename from src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs
rename to src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs
index 7f773c77..c1c9eff4 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GamePathService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs
@@ -2,20 +2,21 @@
// Licensed under the MIT license.
using Snap.Hutao.Service.Game.Locator;
+using Snap.Hutao.Service.Game.PathAbstraction;
-namespace Snap.Hutao.Service.Game;
+namespace Snap.Hutao.Service.Game.PathAbstraction;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IGamePathService))]
internal sealed partial class GamePathService : IGamePathService
{
private readonly IServiceProvider serviceProvider;
- private readonly AppOptions appOptions;
+ private readonly LaunchOptions launchOptions;
public async ValueTask> SilentGetGamePathAsync()
{
// Cannot find in setting
- if (string.IsNullOrEmpty(appOptions.GamePath))
+ if (string.IsNullOrEmpty(launchOptions.GamePath))
{
IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService();
@@ -40,7 +41,7 @@ internal sealed partial class GamePathService : IGamePathService
if (isOk)
{
// Save result.
- appOptions.GamePath = path;
+ launchOptions.UpdateGamePathAndRefreshEntries(path);
}
else
{
@@ -48,9 +49,9 @@ internal sealed partial class GamePathService : IGamePathService
}
}
- if (!string.IsNullOrEmpty(appOptions.GamePath))
+ if (!string.IsNullOrEmpty(launchOptions.GamePath))
{
- return new(true, appOptions.GamePath);
+ return new(true, launchOptions.GamePath);
}
else
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/IGamePathService.cs
similarity index 79%
rename from src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs
rename to src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/IGamePathService.cs
index c0b09ccc..3c01c01c 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGamePathService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/IGamePathService.cs
@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
-namespace Snap.Hutao.Service.Game;
+namespace Snap.Hutao.Service.Game.PathAbstraction;
internal interface IGamePathService
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs
index e9d6e28a..c55ee88e 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs
@@ -52,7 +52,7 @@ internal sealed partial class GameProcessService : IGameProcessService
return;
}
- if (!appOptions.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName))
+ if (!launchOptions.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName))
{
ArgumentException.ThrowIfNullOrEmpty(gamePath);
return; // null check passing, actually never reach.
@@ -73,7 +73,7 @@ internal sealed partial class GameProcessService : IGameProcessService
await Starward.LaunchForPlayTimeStatisticsAsync(isOversea).ConfigureAwait(false);
}
- if (runtimeOptions.IsElevated && appOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps)
+ if (runtimeOptions.IsElevated && launchOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps)
{
progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps));
try
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml
index 9fd729b1..b9deafdf 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml
@@ -38,7 +38,12 @@
-
+
+
+
+
+
+
+ Orientation="Horizontal">
+
+
-
-
-
-
-
- Visible
-
-
-
-
-
-
-
-
-
- Collapsed
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml
index 3a65b2f0..090bf170 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/SettingPage.xaml
@@ -307,20 +307,6 @@
-
-
-
-
-
-
-
-
-
+ Visibility="{Binding LaunchOptions.IsAdvancedLaunchOptionsEnabled, Converter={StaticResource BoolToVisibilityConverter}}"/>
+
-
+
-
+
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs
index 01bb8cdd..9fac23ba 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs
@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
+using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core;
@@ -11,13 +12,15 @@ using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Configuration;
+using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Game.Package;
+using Snap.Hutao.Service.Game.PathAbstraction;
using Snap.Hutao.Service.Game.Scheme;
-using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
+using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
@@ -38,7 +41,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
private readonly IContentDialogFactory contentDialogFactory;
private readonly LaunchStatusOptions launchStatusOptions;
- private readonly INavigationService navigationService;
+ private readonly IGameLocatorFactory gameLocatorFactory;
private readonly IProgressFactory progressFactory;
private readonly IInfoBarService infoBarService;
private readonly ResourceClient resourceClient;
@@ -54,6 +57,9 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
private ObservableCollection? gameAccounts;
private GameAccount? selectedGameAccount;
private GameResource? gameResource;
+ private bool gamePathSelectedAndValid;
+ private ImmutableList gamePathEntries;
+ private GamePathEntry? selectedGamePathEntry;
public List KnownSchemes { get; } = KnownLaunchSchemes.Get();
@@ -99,68 +105,127 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
public GameResource? GameResource { get => gameResource; set => SetProperty(ref gameResource, value); }
- protected override async ValueTask InitializeUIAsync()
+ public bool GamePathSelectedAndValid
{
- if (File.Exists(AppOptions.GamePath))
+ get => gamePathSelectedAndValid;
+ set
{
- try
+ if (SetProperty(ref gamePathSelectedAndValid, value) && value)
{
- using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
+ InitializeUICoreAsync().SafeForget();
+ }
+ }
+ }
+
+ public ImmutableList GamePathEntries { get => gamePathEntries; set => SetProperty(ref gamePathEntries, value); }
+
+ public GamePathEntry? SelectedGamePathEntry
+ {
+ get => selectedGamePathEntry;
+ set => UpdateSelectedGamePathEntry(value, true);
+ }
+
+ protected override ValueTask InitializeUIAsync()
+ {
+ GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
+ SelectedGamePathEntry = entry;
+ return ValueTask.FromResult(true);
+ }
+
+ private async ValueTask InitializeUICoreAsync()
+ {
+ try
+ {
+ using (await EnterCriticalExecutionAsync().ConfigureAwait(false))
+ {
+ ChannelOptions options = gameService.GetChannelOptions();
+ if (string.IsNullOrEmpty(options.ConfigFilePath))
{
- ChannelOptions options = gameService.GetChannelOptions();
- if (string.IsNullOrEmpty(options.ConfigFilePath))
+ try
{
- try
+ SelectedScheme = KnownSchemes.Single(scheme => scheme.Equals(options));
+ }
+ catch (InvalidOperationException)
+ {
+ if (!IgnoredInvalidChannelOptions.Contains(options))
{
- SelectedScheme = KnownSchemes.Single(scheme => scheme.Equals(options));
- }
- catch (InvalidOperationException)
- {
- if (!IgnoredInvalidChannelOptions.Contains(options))
- {
- // 后台收集
- throw new NotSupportedException($"不支持的 MultiChannel: {options}");
- }
+ // 后台收集
+ throw new NotSupportedException($"不支持的 MultiChannel: {options}");
}
}
- else
- {
- infoBarService.Warning(SH.FormatViewModelLaunchGameMultiChannelReadFail(options.ConfigFilePath));
- }
-
- ObservableCollection accounts = gameService.GameAccountCollection;
-
- await taskContext.SwitchToMainThreadAsync();
- GameAccounts = accounts;
-
- // Sync uid
- if (memoryCache.TryRemove(DesiredUid, out object? value) && value is string uid)
- {
- SelectedGameAccount = GameAccounts.FirstOrDefault(g => g.AttachUid == uid);
- }
-
- // Try set to the current account.
- SelectedGameAccount ??= gameService.DetectCurrentGameAccount();
}
- }
- catch (UserdataCorruptedException ex)
- {
- infoBarService.Error(ex);
- }
- catch (OperationCanceledException)
- {
+ else
+ {
+ infoBarService.Warning(SH.FormatViewModelLaunchGameMultiChannelReadFail(options.ConfigFilePath));
+ }
+
+ ObservableCollection accounts = gameService.GameAccountCollection;
+
+ await taskContext.SwitchToMainThreadAsync();
+ GameAccounts = accounts;
+
+ // Sync uid
+ if (memoryCache.TryRemove(DesiredUid, out object? value) && value is string uid)
+ {
+ SelectedGameAccount = GameAccounts.FirstOrDefault(g => g.AttachUid == uid);
+ }
+
+ // Try set to the current account.
+ SelectedGameAccount ??= gameService.DetectCurrentGameAccount();
}
}
- else
+ catch (UserdataCorruptedException ex)
{
- infoBarService.Warning(SH.ViewModelLaunchGamePathInvalid);
- await taskContext.SwitchToMainThreadAsync();
- await navigationService
- .NavigateAsync(INavigationAwaiter.Default, true)
- .ConfigureAwait(false);
+ infoBarService.Error(ex);
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ }
+
+ private void UpdateSelectedGamePathEntry(GamePathEntry? value, bool setBack)
+ {
+ if (SetProperty(ref selectedGamePathEntry, value) && setBack)
+ {
+ launchOptions.GamePath = value?.Path ?? string.Empty;
+ GamePathSelectedAndValid = File.Exists(launchOptions.GamePath);
+ }
+ }
+
+ [Command("SetGamePathCommand")]
+ private async Task SetGamePathAsync()
+ {
+ IGameLocator locator = gameLocatorFactory.Create(GameLocationSource.Manual);
+
+ (bool isOk, string path) = await locator.LocateGamePathAsync().ConfigureAwait(false);
+ if (!isOk)
+ {
+ return;
}
- return true;
+ await taskContext.SwitchToMainThreadAsync();
+ try
+ {
+ GamePathEntries = launchOptions.UpdateGamePathAndRefreshEntries(path);
+ }
+ catch (SqliteException ex)
+ {
+ // 文件夹权限不足,无法写入数据库
+ infoBarService.Error(ex, SH.ViewModelSettingSetGamePathDatabaseFailedTitle);
+ }
+ }
+
+ [Command("ResetGamePathCommand")]
+ private void ResetGamePath()
+ {
+ SelectedGamePathEntry = default;
+ }
+
+ [Command("RemoveGamePathEntryCommand")]
+ private void RemoveGamePathEntry(GamePathEntry? entry)
+ {
+ GamePathEntries = launchOptions.RemoveGamePathEntry(entry, out GamePathEntry? selected);
+ SelectedGamePathEntry = selected;
}
[Command("LaunchCommand")]
@@ -187,6 +252,11 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail);
return;
}
+ else
+ {
+ GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
+ UpdateSelectedGamePathEntry(entry, false);
+ }
}
if (SelectedGameAccount is not null)
@@ -265,7 +335,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
[Command("OpenScreenshotFolderCommand")]
private async Task OpenScreenshotFolderAsync()
{
- string game = appOptions.GamePath;
+ string game = LaunchOptions.GamePath;
string? directory = Path.GetDirectoryName(game);
ArgumentException.ThrowIfNullOrEmpty(directory);
string screenshot = Path.Combine(directory, "ScreenShot");
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs
index 6b09771c..d72538c3 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs
@@ -16,6 +16,7 @@ using Snap.Hutao.Factory.Picker;
using Snap.Hutao.Model;
using Snap.Hutao.Service;
using Snap.Hutao.Service.GachaLog.QueryProvider;
+using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Locator;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Navigation;
@@ -53,6 +54,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
private readonly HutaoUserOptions hutaoUserOptions;
private readonly IInfoBarService infoBarService;
private readonly RuntimeOptions runtimeOptions;
+ private readonly LaunchOptions launchOptions;
private readonly HotKeyOptions hotKeyOptions;
private readonly IUserService userService;
private readonly ITaskContext taskContext;
@@ -74,6 +76,8 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
public HotKeyOptions HotKeyOptions { get => hotKeyOptions; }
+ public LaunchOptions LaunchOptions { get => launchOptions; }
+
public HutaoPassportViewModel Passport { get => hutaoPassportViewModel; }
public NameValue? SelectedBackdropType
@@ -157,27 +161,6 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
await Launcher.LaunchUriAsync(new("ms-windows-store://pdp/?productid=9PH4NXJ2JN52"));
}
- [Command("SetGamePathCommand")]
- private async Task SetGamePathAsync()
- {
- IGameLocator locator = gameLocatorFactory.Create(GameLocationSource.Manual);
-
- (bool isOk, string path) = await locator.LocateGamePathAsync().ConfigureAwait(false);
- if (isOk)
- {
- await taskContext.SwitchToMainThreadAsync();
- try
- {
- AppOptions.GamePath = path;
- }
- catch (SqliteException ex)
- {
- // 文件夹权限不足,无法写入数据库
- infoBarService.Error(ex, SH.ViewModelSettingSetGamePathDatabaseFailedTitle);
- }
- }
- }
-
[Command("SetPowerShellPathCommand")]
private async Task SetPowerShellPathAsync()
{
@@ -193,7 +176,7 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
[Command("DeleteGameWebCacheCommand")]
private void DeleteGameWebCache()
{
- string gamePath = AppOptions.GamePath;
+ string gamePath = launchOptions.GamePath;
if (!string.IsNullOrEmpty(gamePath))
{