diff --git a/BeatSaberModManager/Program.cs b/BeatSaberModManager/Program.cs index ec6de75..77b86ce 100644 --- a/BeatSaberModManager/Program.cs +++ b/BeatSaberModManager/Program.cs @@ -156,7 +156,7 @@ internal static class AssetProvidersModule; [Register(Scope.SingleInstance)] [Register(Scope.SingleInstance)] [Register(Scope.SingleInstance)] - [Register(typeof(SteamAuthenticationViewModel), Scope.SingleInstance, typeof(SteamAuthenticationViewModel), typeof(ISteamAuthenticator))] + [Register(Scope.SingleInstance)] [Register(Scope.SingleInstance)] internal static class ViewModelModule; diff --git a/BeatSaberModManager/Resources/Localization/de.axaml b/BeatSaberModManager/Resources/Localization/de.axaml index d23edd4..88761d1 100644 --- a/BeatSaberModManager/Resources/Localization/de.axaml +++ b/BeatSaberModManager/Resources/Localization/de.axaml @@ -87,6 +87,9 @@ Absenden Cancel + Benutze die Steam App, um deine Anmeldung zu bestätigen + Anmeldung Bestätigen + Installiere: Deinstalliere: Installation abgeschlossen diff --git a/BeatSaberModManager/Resources/Localization/en.axaml b/BeatSaberModManager/Resources/Localization/en.axaml index f3c97b7..1885cbb 100644 --- a/BeatSaberModManager/Resources/Localization/en.axaml +++ b/BeatSaberModManager/Resources/Localization/en.axaml @@ -87,6 +87,9 @@ Submit Cancel + Use the Steam mobile app to confirm your sign in + Confirm Login + Installing: Uninstalling: Installation completed diff --git a/BeatSaberModManager/Services/Implementations/GameVersions/Steam/SteamLegacyGameVersionInstaller.cs b/BeatSaberModManager/Services/Implementations/GameVersions/Steam/SteamLegacyGameVersionInstaller.cs index ada9f66..41da9cd 100644 --- a/BeatSaberModManager/Services/Implementations/GameVersions/Steam/SteamLegacyGameVersionInstaller.cs +++ b/BeatSaberModManager/Services/Implementations/GameVersions/Steam/SteamLegacyGameVersionInstaller.cs @@ -16,20 +16,26 @@ namespace BeatSaberModManager.Services.Implementations.GameVersions.Steam { /// - public class SteamLegacyGameVersionInstaller(Steam3Session steam3Session, ISteamAuthenticator steamAuthenticator, ILogger logger) : ILegacyGameVersionInstaller + public class SteamLegacyGameVersionInstaller(Steam3Session steam3Session, ILogger logger) : ILegacyGameVersionInstaller { /// - public async Task InstallLegacyGameVersionAsync(IGameVersion gameVersion, CancellationToken cancellationToken, IProgress? progress = null) + public async Task InstallLegacyGameVersionAsync(IGameVersion gameVersion, ILegacyGameVersionAuthenticator authenticator, CancellationToken cancellationToken, IProgress? progress = null) { ArgumentNullException.ThrowIfNull(gameVersion); - if (gameVersion is not SteamGameVersion steamLegacyGameVersion) + if (gameVersion is not SteamGameVersion steamLegacyGameVersion || authenticator is not ISteamAuthenticator steamAuthenticator) return null; string appDataDirPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); string legacyGameVersionsDirPath = Path.Join(appDataDirPath, ThisAssembly.Info.Product, "LegacyGameVersions", gameVersion.GameVersion); DownloadConfig downloadConfig = new() { InstallDirectory = legacyGameVersionsDirPath, MaxDownloads = 3 }; - await steam3Session.LoginAsync(steamAuthenticator, cancellationToken).ConfigureAwait(false); - if (cancellationToken.IsCancellationRequested) + try + { + await steam3Session.LoginAsync(steamAuthenticator, cancellationToken).ConfigureAwait(false); + } + catch (TaskCanceledException) + { return null; + } + ContentDownloader contentDownloader = new(downloadConfig, steam3Session); List<(uint DepotId, ulong ManifestId)> depotManifestIds = [(620981, steamLegacyGameVersion.ManifestId)]; try @@ -51,7 +57,6 @@ public class SteamLegacyGameVersionInstaller(Steam3Session steam3Session, ISteam /// public Task UninstallLegacyGameVersionAsync(IGameVersion gameVersion) { - // TODO: doesn't work ArgumentNullException.ThrowIfNull(gameVersion); if (gameVersion is not SteamGameVersion) return Task.FromResult(false); diff --git a/BeatSaberModManager/Services/Interfaces/ILegacyGameVersionAuthenticator.cs b/BeatSaberModManager/Services/Interfaces/ILegacyGameVersionAuthenticator.cs new file mode 100644 index 0000000..1b3d245 --- /dev/null +++ b/BeatSaberModManager/Services/Interfaces/ILegacyGameVersionAuthenticator.cs @@ -0,0 +1,9 @@ +namespace BeatSaberModManager.Services.Interfaces +{ + /// + /// TODO + /// +#pragma warning disable CA1040 + public interface ILegacyGameVersionAuthenticator; +#pragma warning restore CA1040 +} diff --git a/BeatSaberModManager/Services/Interfaces/ILegacyGameVersionInstaller.cs b/BeatSaberModManager/Services/Interfaces/ILegacyGameVersionInstaller.cs index e02999c..5412e73 100644 --- a/BeatSaberModManager/Services/Interfaces/ILegacyGameVersionInstaller.cs +++ b/BeatSaberModManager/Services/Interfaces/ILegacyGameVersionInstaller.cs @@ -16,10 +16,11 @@ public interface ILegacyGameVersionInstaller /// /// /// + /// /// /// /// - Task InstallLegacyGameVersionAsync(IGameVersion gameVersion, CancellationToken cancellationToken, IProgress? progress = null); + Task InstallLegacyGameVersionAsync(IGameVersion gameVersion, ILegacyGameVersionAuthenticator authenticator, CancellationToken cancellationToken, IProgress? progress = null); /// /// diff --git a/BeatSaberModManager/ViewModels/LegacyGameVersionsViewModel.cs b/BeatSaberModManager/ViewModels/LegacyGameVersionsViewModel.cs index b111a64..c4d6de1 100644 --- a/BeatSaberModManager/ViewModels/LegacyGameVersionsViewModel.cs +++ b/BeatSaberModManager/ViewModels/LegacyGameVersionsViewModel.cs @@ -20,7 +20,7 @@ namespace BeatSaberModManager.ViewModels /// /// TODO /// - public sealed class LegacyGameVersionsViewModel : ViewModelBase + public sealed class LegacyGameVersionsViewModel : ViewModelBase, IDisposable { private readonly IGameInstallLocator _gameInstallLocator; private readonly IInstallDirValidator _installDirValidator; @@ -32,6 +32,8 @@ public sealed class LegacyGameVersionsViewModel : ViewModelBase private readonly ObservableAsPropertyHelper _canUninstall; private readonly ObservableAsPropertyHelper _legacyGameVersions; + private CancellationTokenSource? _installLegacyVersionCts; + /// /// TODO /// @@ -145,9 +147,12 @@ public GameVersionViewModel? SelectedGameVersion private async Task?> GetLegacyGameVersionsAsync() { - IReadOnlyList? availableGameVersions = await _legacyGameVersionProvider.GetAvailableGameVersionsAsync().ConfigureAwait(false); - IReadOnlyList? installedLegacyGameVersions = await _legacyGameVersionProvider.GetInstalledLegacyGameVersionsAsync().ConfigureAwait(false); - IGameVersion? installedStoreVersion = await _gameInstallLocator.LocateGameInstallAsync().ConfigureAwait(false); + Task?> t1 = _legacyGameVersionProvider.GetAvailableGameVersionsAsync(); + Task?> t2 = _legacyGameVersionProvider.GetInstalledLegacyGameVersionsAsync(); + Task t3 = _gameInstallLocator.LocateGameInstallAsync(); + IReadOnlyList? availableGameVersions = await t1.ConfigureAwait(false); + IReadOnlyList? installedLegacyGameVersions = await t2.ConfigureAwait(false); + IGameVersion? installedStoreVersion = await t3.ConfigureAwait(false); List allInstalledGameVersions = installedLegacyGameVersions?.ToList() ?? []; if (installedStoreVersion is not null) allInstalledGameVersions.Insert(0, installedStoreVersion); @@ -166,11 +171,12 @@ public GameVersionViewModel? SelectedGameVersion private async Task InstallSelectedLegacyGameVersionAsync() { + _installLegacyVersionCts?.Dispose(); + _installLegacyVersionCts = new CancellationTokenSource(); GameVersionViewModel selectedGameVersion = SelectedGameVersion!; StatusProgress.Report(new ProgressInfo(StatusType.Installing, selectedGameVersion.GameVersion.GameVersion)); - using CancellationTokenSource cts = new(); - using IDisposable subscription = SteamAuthenticationViewModel.CancelCommand.Subscribe(_ => cts.Cancel()); - string? installDir = await _legacyGameVersionInstaller.InstallLegacyGameVersionAsync(selectedGameVersion.GameVersion, cts.Token, StatusProgress).ConfigureAwait(false); + using IDisposable subscription = SteamAuthenticationViewModel.CancelCommand.Subscribe(_ => _installLegacyVersionCts.Cancel()); + string? installDir = await Task.Run(() => _legacyGameVersionInstaller.InstallLegacyGameVersionAsync(selectedGameVersion.GameVersion, SteamAuthenticationViewModel, _installLegacyVersionCts.Token, StatusProgress)).ConfigureAwait(false); if (installDir is null) { StatusProgress.Report(new ProgressInfo(StatusType.Failed, null)); @@ -205,5 +211,12 @@ private void MarkStoreVersionAsInstalled(IGameVersion storeVersion, IEnumerable< break; } } + + /// + public void Dispose() + { + _installLegacyVersionCts?.Cancel(); + _installLegacyVersionCts?.Dispose(); + } } } diff --git a/BeatSaberModManager/ViewModels/SteamAuthenticationViewModel.cs b/BeatSaberModManager/ViewModels/SteamAuthenticationViewModel.cs index 5ef132c..06c692a 100644 --- a/BeatSaberModManager/ViewModels/SteamAuthenticationViewModel.cs +++ b/BeatSaberModManager/ViewModels/SteamAuthenticationViewModel.cs @@ -5,6 +5,8 @@ using System.Threading; using System.Threading.Tasks; +using BeatSaberModManager.Services.Interfaces; + using LibDepotDownloader; using ReactiveUI; @@ -17,11 +19,13 @@ namespace BeatSaberModManager.ViewModels /// /// /// - public sealed class SteamAuthenticationViewModel : ViewModelBase, ISteamAuthenticator, IDisposable + public sealed class SteamAuthenticationViewModel : ViewModelBase, ILegacyGameVersionAuthenticator, ISteamAuthenticator, IDisposable { private readonly Steam3Session _session; - private CancellationTokenSource? _cancellationTokenSource; + private CancellationTokenSource? _credentialsAuthSessionCts; + private CancellationTokenSource? _qrAuthSessionCts; + private CancellationTokenSource? _deviceConfirmationCts; /// /// @@ -32,27 +36,32 @@ public SteamAuthenticationViewModel(Steam3Session session) _session = session; IObservable canLogin = this.WhenAnyValue(static x => x.Username, static x => x.Password) .Select(static x => !string.IsNullOrWhiteSpace(x.Item1) && !string.IsNullOrWhiteSpace(x.Item2)); - LoginCommand = ReactiveCommand.CreateFromTask(LoginWithCredentialsAsync, canLogin); - CancelCommand = ReactiveCommand.Create(() => _cancellationTokenSource?.Cancel()); + LoginCommand = ReactiveCommand.Create(() => { _qrAuthSessionCts?.Cancel(); }, canLogin); + CancelCommand = ReactiveCommand.Create(() => { _credentialsAuthSessionCts?.Cancel(); _qrAuthSessionCts?.Cancel(); }); IObservable canSubmitSteamGuardCode = this.WhenAnyValue(static x => x.SteamGuardCode) .Select(static code => code is not null && code.Length == 5); SubmitSteamGuardCodeCommand = ReactiveCommand.Create(() => SteamGuardCode!, canSubmitSteamGuardCode); } /// - /// TODO + /// + /// + public Interaction AuthenticateInteraction { get; } = new(RxApp.MainThreadScheduler); + + /// + /// /// - public Interaction AuthenticateSteamInteraction { get; } = new(RxApp.MainThreadScheduler); + public Interaction DeviceConfirmationInteraction { get; } = new(RxApp.MainThreadScheduler); /// /// /// - public Interaction GetSteamGuardCodeInteraction { get; } = new(RxApp.MainThreadScheduler); + public Interaction GetDeviceCodeInteraction { get; } = new(RxApp.MainThreadScheduler); /// /// /// - public ReactiveCommand LoginCommand { get; } + public ReactiveCommand LoginCommand { get; } /// /// @@ -108,6 +117,17 @@ public bool RememberPassword private bool _rememberPassword; + /// + /// + /// + public bool IsPasswordInvalid + { + get => _isPasswordInvalid; + set => this.RaiseAndSetIfChanged(ref _isPasswordInvalid, value); + } + + private bool _isPasswordInvalid; + /// /// /// @@ -120,82 +140,74 @@ public string? SteamGuardCode private string? _steamGuardCode; /// - public Task GetDeviceCodeAsync(bool previousCodeWasIncorrect) => GetSteamGuardCodeInteraction.Handle(previousCodeWasIncorrect).ToTask(); + public void Dispose() + { + _credentialsAuthSessionCts?.Dispose(); + _qrAuthSessionCts?.Dispose(); + _deviceConfirmationCts?.Dispose(); + } /// - public Task GetEmailCodeAsync(string email, bool previousCodeWasIncorrect) => throw new System.NotImplementedException(); + public Task GetDeviceCodeAsync(bool previousCodeWasIncorrect) => GetDeviceCodeInteraction.Handle(Unit.Default).ToTask()!; /// - public Task AcceptDeviceConfirmationAsync() => Task.FromResult(false); - - /// - /// - /// - /// - public Task AuthenticateAsync() => AuthenticateSteamInteraction.Handle(Unit.Default).ToTask(); + public Task GetEmailCodeAsync(string email, bool previousCodeWasIncorrect) => throw new NotImplementedException(); - /// - /// - /// - /// - public Task StartAuthenticationAsync() + /// + public Task AcceptDeviceConfirmationAsync() { - _cancellationTokenSource?.Dispose(); - _cancellationTokenSource = new CancellationTokenSource(); + if (_qrAuthSessionCts!.IsCancellationRequested) + DeviceConfirmationInteraction.Handle(_deviceConfirmationCts!.Token).Subscribe(); - return Observable.StartAsync(LoginWithQrCodeAsync) - .Merge(LoginCommand) - .FirstAsync() - .ToTask(); + return Task.FromResult(true); } /// - public void Dispose() + public async Task AuthenticateAsync() { - _cancellationTokenSource?.Dispose(); - } + _credentialsAuthSessionCts?.Dispose(); + _qrAuthSessionCts?.Dispose(); + _deviceConfirmationCts?.Dispose(); + _credentialsAuthSessionCts = new CancellationTokenSource(); + _qrAuthSessionCts = new CancellationTokenSource(); + _deviceConfirmationCts = new CancellationTokenSource(); - private async Task LoginWithCredentialsAsync() - { - AuthSessionDetails details = new() - { - Authenticator = this, - Username = Username, - Password = Password, - IsPersistentSession = RememberPassword - }; + IsPasswordInvalid = false; - CredentialsAuthSession authSession = await _session.SteamClient.Authentication.BeginAuthSessionViaCredentialsAsync(details).ConfigureAwait(false); + AuthenticateInteraction.Handle(_deviceConfirmationCts.Token).Subscribe(); + + AuthSessionDetails details = new() { Authenticator = this }; try { - AuthPollResult result = await authSession.PollingWaitForResultAsync(_cancellationTokenSource!.Token).ConfigureAwait(false); - await _cancellationTokenSource.CancelAsync().ConfigureAwait(false); + QrAuthSession authSession = await _session.SteamClient.Authentication.BeginAuthSessionViaQRAsync(details).ConfigureAwait(false); + LoginChallenge = authSession.ChallengeURL; + authSession.ChallengeURLChanged += () => LoginChallenge = authSession.ChallengeURL; + AuthPollResult result = await authSession.PollingWaitForResultAsync(_qrAuthSessionCts.Token).ConfigureAwait(false); + await _deviceConfirmationCts.CancelAsync().ConfigureAwait(false); return result; } - catch (TaskCanceledException) - { - return null; - } - } + catch (TaskCanceledException) { } - private async Task LoginWithQrCodeAsync() - { - AuthSessionDetails details = new() - { - Authenticator = this - }; + if (_credentialsAuthSessionCts.IsCancellationRequested) + return null; - QrAuthSession authSession = await _session.SteamClient.Authentication.BeginAuthSessionViaQRAsync(details).ConfigureAwait(false); - LoginChallenge = authSession.ChallengeURL; - authSession.ChallengeURLChanged += () => LoginChallenge = authSession.ChallengeURL; + details.Username = Username; + details.Password = Password; + details.IsPersistentSession = RememberPassword; try { - AuthPollResult result = await authSession.PollingWaitForResultAsync(_cancellationTokenSource!.Token).ConfigureAwait(false); - await _cancellationTokenSource.CancelAsync().ConfigureAwait(false); + CredentialsAuthSession authSession = await _session.SteamClient.Authentication.BeginAuthSessionViaCredentialsAsync(details).ConfigureAwait(false); + AuthPollResult result = await authSession.PollingWaitForResultAsync(_credentialsAuthSessionCts.Token).ConfigureAwait(false); + await _deviceConfirmationCts.CancelAsync().ConfigureAwait(false); return result; } + catch (AuthenticationException) + { + IsPasswordInvalid = true; + return null; + } catch (TaskCanceledException) { return null; diff --git a/BeatSaberModManager/Views/Converters/IsPasswordInvalidColorConverter.cs b/BeatSaberModManager/Views/Converters/IsPasswordInvalidColorConverter.cs new file mode 100644 index 0000000..ffa99bb --- /dev/null +++ b/BeatSaberModManager/Views/Converters/IsPasswordInvalidColorConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; + +using Avalonia.Data.Converters; +using Avalonia.Media; + + +namespace BeatSaberModManager.Views.Converters +{ + /// + /// + /// + public class IsPasswordInvalidColorConverter : IValueConverter + { + /// + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not bool isPasswordInvalid) + throw new InvalidCastException(); + return isPasswordInvalid ? Brushes.Red : Brushes.Transparent; + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new InvalidOperationException(); + } +} diff --git a/BeatSaberModManager/Views/Dialogs/SteamGuardCodeView.axaml b/BeatSaberModManager/Views/Dialogs/SteamGuardCodeView.axaml index 8428e09..5f4d994 100644 --- a/BeatSaberModManager/Views/Dialogs/SteamGuardCodeView.axaml +++ b/BeatSaberModManager/Views/Dialogs/SteamGuardCodeView.axaml @@ -7,12 +7,12 @@ x:Class="BeatSaberModManager.Views.Dialogs.SteamGuardCodeView" x:DataType="vm:SteamAuthenticationViewModel"> - - + + - + diff --git a/BeatSaberModManager/Views/Dialogs/SteamLoginView.axaml b/BeatSaberModManager/Views/Dialogs/SteamLoginView.axaml index 405a46f..4d501d2 100644 --- a/BeatSaberModManager/Views/Dialogs/SteamLoginView.axaml +++ b/BeatSaberModManager/Views/Dialogs/SteamLoginView.axaml @@ -4,16 +4,21 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:BeatSaberModManager.ViewModels" xmlns:qr="using:Avalonia.Labs.Qr" + xmlns:converters="using:BeatSaberModManager.Views.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="BeatSaberModManager.Views.Dialogs.SteamLoginView" x:DataType="vm:SteamAuthenticationViewModel"> + + + +