mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
fix startup crash
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// 指示此特性附加的属性会在属性改变后会异步地设置的其他属性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
|
||||
internal sealed class AlsoAsyncSetsAttribute : Attribute
|
||||
{
|
||||
public AlsoAsyncSetsAttribute(string propertyName)
|
||||
{
|
||||
}
|
||||
|
||||
public AlsoAsyncSetsAttribute(string propertyName1, string propertyName2)
|
||||
{
|
||||
}
|
||||
|
||||
public AlsoAsyncSetsAttribute(params string[] propertyNames)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// 指示此特性附加的属性会在属性改变后会设置的其他属性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
|
||||
internal sealed class AlsoSetsAttribute : Attribute
|
||||
{
|
||||
public AlsoSetsAttribute(string propertyName)
|
||||
{
|
||||
}
|
||||
|
||||
public AlsoSetsAttribute(string propertyName1, string propertyName2)
|
||||
{
|
||||
}
|
||||
|
||||
public AlsoSetsAttribute(params string[] propertyNames)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
internal sealed class HutaoException : Exception
|
||||
{
|
||||
public HutaoException(HutaoExceptionKind kind, string message, Exception? innerException)
|
||||
: this(message, innerException)
|
||||
{
|
||||
Kind = kind;
|
||||
}
|
||||
|
||||
public HutaoException(string message, Exception? innerException)
|
||||
: base($"{message}\n{innerException?.Message}", innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public HutaoExceptionKind Kind { get; private set; }
|
||||
}
|
||||
|
||||
internal enum HutaoExceptionKind
|
||||
{
|
||||
None,
|
||||
|
||||
}
|
||||
@@ -198,7 +198,7 @@
|
||||
<Border Style="{StaticResource BorderCardStyle}">
|
||||
<ListView
|
||||
ItemTemplate="{StaticResource GameAccountListTemplate}"
|
||||
ItemsSource="{Binding GameAccounts}"
|
||||
ItemsSource="{Binding GameAccountsView}"
|
||||
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.WinUI.Collections;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.Diagnostics.CodeAnalysis;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Factory.Progress;
|
||||
@@ -64,8 +64,17 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
private GamePathEntry? selectedGamePathEntry;
|
||||
private GameAccountFilter? gameAccountFilter;
|
||||
|
||||
public LaunchOptions LaunchOptions { get => launchOptions; }
|
||||
|
||||
public LaunchStatusOptions LaunchStatusOptions { get => launchStatusOptions; }
|
||||
|
||||
public RuntimeOptions RuntimeOptions { get => runtimeOptions; }
|
||||
|
||||
public AppOptions AppOptions { get => appOptions; }
|
||||
|
||||
public List<LaunchScheme> KnownSchemes { get; } = KnownLaunchSchemes.Get();
|
||||
|
||||
[AlsoAsyncSets(nameof(GameResource), nameof(GameAccountsView))]
|
||||
public LaunchScheme? SelectedScheme
|
||||
{
|
||||
get => selectedScheme;
|
||||
@@ -76,16 +85,9 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
|
||||
public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); }
|
||||
|
||||
public LaunchOptions LaunchOptions { get => launchOptions; }
|
||||
|
||||
public LaunchStatusOptions LaunchStatusOptions { get => launchStatusOptions; }
|
||||
|
||||
public RuntimeOptions RuntimeOptions { get => runtimeOptions; }
|
||||
|
||||
public AppOptions AppOptions { get => appOptions; }
|
||||
|
||||
public GameResource? GameResource { get => gameResource; set => SetProperty(ref gameResource, value); }
|
||||
|
||||
[AlsoAsyncSets(nameof(SelectedScheme), nameof(GameAccountsView))]
|
||||
public bool GamePathSelectedAndValid
|
||||
{
|
||||
get => gamePathSelectedAndValid;
|
||||
@@ -106,24 +108,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
await SetSelectedSchemeAsync(scheme).ConfigureAwait(true);
|
||||
|
||||
// Sync uid, almost never hit, so we are not so care about performance
|
||||
if (memoryCache.TryRemove(DesiredUid, out object? value) && value is string uid)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(GameAccountsView);
|
||||
|
||||
// Exists in the source collection
|
||||
if (GameAccountsView.SourceCollection.Cast<GameAccount>().FirstOrDefault(g => g.AttachUid == uid) is { } sourceAccount)
|
||||
{
|
||||
SelectedGameAccount = GameAccountsView.Cast<GameAccount>().FirstOrDefault(g => g.AttachUid == uid);
|
||||
|
||||
// But not exists in the view for current scheme
|
||||
if (SelectedGameAccount is null)
|
||||
{
|
||||
infoBarService.Warning(SH.FormatViewModelLaunchGameUnableToSwitchUidAttachedGameAccount(uid, sourceAccount.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
TrySetGameAccountByDesiredUid();
|
||||
|
||||
// Try set to the current account.
|
||||
if (SelectedScheme is not null)
|
||||
@@ -142,59 +127,68 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
infoBarService.Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
void TrySetGameAccountByDesiredUid()
|
||||
{
|
||||
// Sync uid, almost never hit, so we are not so care about performance
|
||||
if (memoryCache.TryRemove(DesiredUid, out object? value) && value is string uid)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(GameAccountsView);
|
||||
|
||||
// Exists in the source collection
|
||||
if (GameAccountsView.SourceCollection.Cast<GameAccount>().FirstOrDefault(g => g.AttachUid == uid) is { } sourceAccount)
|
||||
{
|
||||
SelectedGameAccount = GameAccountsView.Cast<GameAccount>().FirstOrDefault(g => g.AttachUid == uid);
|
||||
|
||||
// But not exists in the view for current scheme
|
||||
if (SelectedGameAccount is null)
|
||||
{
|
||||
infoBarService.Warning(SH.FormatViewModelLaunchGameUnableToSwitchUidAttachedGameAccount(uid, sourceAccount.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ImmutableList<GamePathEntry> GamePathEntries { get => gamePathEntries; set => SetProperty(ref gamePathEntries, value); }
|
||||
|
||||
[AlsoSets(nameof(GamePathSelectedAndValid))]
|
||||
public GamePathEntry? SelectedGamePathEntry
|
||||
{
|
||||
get => selectedGamePathEntry;
|
||||
set => UpdateSelectedGamePathEntry(value, true);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref selectedGamePathEntry, value, nameof(SelectedGamePathEntry)))
|
||||
{
|
||||
if (IsViewDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
launchOptions.GamePath = value?.Path ?? string.Empty;
|
||||
GamePathSelectedAndValid = File.Exists(launchOptions.GamePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override ValueTask<bool> InitializeUIAsync()
|
||||
{
|
||||
GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
|
||||
SelectedGamePathEntry = entry;
|
||||
SyncGamePathEntriesAndSelectedGamePathEntryFromLaunchOptions();
|
||||
return ValueTask.FromResult(true);
|
||||
}
|
||||
|
||||
private void UpdateSelectedGamePathEntry(GamePathEntry? value, bool setBack)
|
||||
{
|
||||
if (SetProperty(ref selectedGamePathEntry, value, nameof(SelectedGamePathEntry)) && setBack)
|
||||
{
|
||||
if (IsViewDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
launchOptions.GamePath = value?.Path ?? string.Empty;
|
||||
GamePathSelectedAndValid = File.Exists(launchOptions.GamePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("SetGamePathCommand")]
|
||||
private async Task SetGamePathAsync()
|
||||
{
|
||||
IGameLocator locator = gameLocatorFactory.Create(GameLocationSource.Manual);
|
||||
|
||||
(bool isOk, string path) = await locator.LocateGamePathAsync().ConfigureAwait(false);
|
||||
(bool isOk, string path) = await gameLocatorFactory.LocateAsync(GameLocationSource.Manual).ConfigureAwait(false);
|
||||
if (!isOk)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
try
|
||||
{
|
||||
GamePathEntries = launchOptions.UpdateGamePathAndRefreshEntries(path);
|
||||
}
|
||||
catch (SqliteException ex)
|
||||
{
|
||||
// 文件夹权限不足,无法写入数据库
|
||||
infoBarService.Error(ex, SH.ViewModelSettingSetGamePathDatabaseFailedTitle);
|
||||
}
|
||||
GamePathEntries = launchOptions.UpdateGamePathAndRefreshEntries(path);
|
||||
}
|
||||
|
||||
[Command("ResetGamePathCommand")]
|
||||
@@ -237,18 +231,14 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
else
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
|
||||
UpdateSelectedGamePathEntry(entry, true);
|
||||
SyncGamePathEntriesAndSelectedGamePathEntryFromLaunchOptions();
|
||||
}
|
||||
}
|
||||
|
||||
if (SelectedGameAccount is not null)
|
||||
if (SelectedGameAccount is not null && !gameService.SetGameAccount(SelectedGameAccount))
|
||||
{
|
||||
if (!gameService.SetGameAccount(SelectedGameAccount))
|
||||
{
|
||||
infoBarService.Warning(SH.ViewModelLaunchGameSwitchGameAccountFail);
|
||||
return;
|
||||
}
|
||||
infoBarService.Warning(SH.ViewModelLaunchGameSwitchGameAccountFail);
|
||||
return;
|
||||
}
|
||||
|
||||
IProgress<LaunchStatus> launchProgress = progressFactory.CreateForMainThread<LaunchStatus>(status => launchStatusOptions.LaunchStatus = status);
|
||||
@@ -377,4 +367,10 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncGamePathEntriesAndSelectedGamePathEntryFromLaunchOptions()
|
||||
{
|
||||
GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry);
|
||||
SelectedGamePathEntry = entry;
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,7 @@ using CommunityToolkit.WinUI.Collections;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Factory.Progress;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using Snap.Hutao.Service.Game;
|
||||
using Snap.Hutao.Service.Game.Configuration;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -43,6 +41,7 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
|
||||
protected override async Task OpenUIAsync()
|
||||
{
|
||||
LaunchScheme? scheme = LaunchGameShared.GetCurrentLaunchSchemeFromConfigFile(gameService, infoBarService);
|
||||
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -58,7 +57,6 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
|
||||
}
|
||||
|
||||
gameAccountFilter = new(scheme?.GetSchemeType());
|
||||
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
GameAccountsView = new(accounts, true)
|
||||
|
||||
@@ -15,7 +15,6 @@ using Snap.Hutao.Model;
|
||||
using Snap.Hutao.Service;
|
||||
using Snap.Hutao.Service.GachaLog.QueryProvider;
|
||||
using Snap.Hutao.Service.Game;
|
||||
using Snap.Hutao.Service.Game.Locator;
|
||||
using Snap.Hutao.Service.Hutao;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
|
||||
Reference in New Issue
Block a user