diff --git a/Directory.Packages.props b/Directory.Packages.props index d6bf9adc9..cc46cec24 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,6 +5,7 @@ + @@ -17,15 +18,12 @@ - - - diff --git a/SS14.Launcher/ConfigConstants.cs b/SS14.Launcher/ConfigConstants.cs index 48147a5a8..da74f0dbe 100644 --- a/SS14.Launcher/ConfigConstants.cs +++ b/SS14.Launcher/ConfigConstants.cs @@ -11,7 +11,7 @@ public static class ConfigConstants // Refresh login tokens if they're within of expiry. public static readonly TimeSpan TokenRefreshThreshold = TimeSpan.FromDays(15); - // If the user leaves the launcher running for absolute ages, this is how often we'll update his login tokens. + // If the user leaves the launcher running for absolute ages, this is how often we'll update their login tokens. public static readonly TimeSpan TokenRefreshInterval = TimeSpan.FromDays(7); // The amount of time before a server is considered timed out for status checks. diff --git a/SS14.Launcher/FodyWeavers.xml b/SS14.Launcher/FodyWeavers.xml deleted file mode 100644 index 63fc14848..000000000 --- a/SS14.Launcher/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/SS14.Launcher/Models/Connector.cs b/SS14.Launcher/Models/Connector.cs index 3abae1f29..49fbb94b9 100644 --- a/SS14.Launcher/Models/Connector.cs +++ b/SS14.Launcher/Models/Connector.cs @@ -13,7 +13,7 @@ using System.Threading.Tasks; using Avalonia.Platform.Storage; using DynamicData; -using ReactiveUI; +using Microsoft.Toolkit.Mvvm.ComponentModel; using Serilog; using Splat; using SS14.Launcher.Models.Data; @@ -27,48 +27,21 @@ namespace SS14.Launcher.Models; /// Responsible for actually launching the game. /// Either by connecting to a game server, or by launching a local content bundle. /// -public partial class Connector : ReactiveObject +public partial class Connector : ObservableObject { - private readonly Updater _updater; - private readonly DataManager _cfg; - private readonly LoginManager _loginManager; - private readonly IEngineManager _engineManager; + private readonly Updater _updater = Locator.Current.GetRequiredService(); + private readonly DataManager _cfg = Locator.Current.GetRequiredService(); + private readonly LoginManager _loginManager = Locator.Current.GetRequiredService(); + private readonly IEngineManager _engineManager = Locator.Current.GetRequiredService(); - private ConnectionStatus _status = ConnectionStatus.None; - private bool _clientExitedBadly; - private readonly HttpClient _http; + private readonly HttpClient _http = Locator.Current.GetRequiredService(); private TaskCompletionSource? _acceptPrivacyPolicyTcs; - private ServerPrivacyPolicyInfo? _serverPrivacyPolicyInfo; - private bool _privacyPolicyDifferentVersion; - public Connector() - { - _updater = Locator.Current.GetRequiredService(); - _cfg = Locator.Current.GetRequiredService(); - _loginManager = Locator.Current.GetRequiredService(); - _engineManager = Locator.Current.GetRequiredService(); - _http = Locator.Current.GetRequiredService(); - } - - public ConnectionStatus Status - { - get => _status; - private set => this.RaiseAndSetIfChanged(ref _status, value); - } - - public bool ClientExitedBadly - { - get => _clientExitedBadly; - private set => this.RaiseAndSetIfChanged(ref _clientExitedBadly, value); - } - - public ServerPrivacyPolicyInfo? PrivacyPolicyInfo => _serverPrivacyPolicyInfo; - public bool PrivacyPolicyDifferentVersion - { - get => _privacyPolicyDifferentVersion; - private set => this.RaiseAndSetIfChanged(ref _privacyPolicyDifferentVersion, value); - } + [ObservableProperty] private ConnectionStatus _status = ConnectionStatus.None; + [ObservableProperty] private bool _clientExitedBadly; + [ObservableProperty] private bool _privacyPolicyDifferentVersion; + public ServerPrivacyPolicyInfo? PrivacyPolicyInfo { get; private set; } public async void Connect(string address, CancellationToken cancel = default) { @@ -168,7 +141,7 @@ private async Task HandlePrivacyPolicyAsync(ServerInfo info, CancellationToken c // Ask user for privacy policy acceptance by waiting here. Log.Debug("Prompting user for privacy policy acceptance: {Identifer} version {Version}", identifier, version); - _serverPrivacyPolicyInfo = info.PrivacyPolicy; + PrivacyPolicyInfo = info.PrivacyPolicy; _acceptPrivacyPolicyTcs = new TaskCompletionSource(); Status = ConnectionStatus.AwaitingPrivacyPolicyAcceptance; @@ -203,7 +176,7 @@ public void ConfirmPrivacyPolicy(PrivacyPolicyAcceptResult result) private void Cleanup() { - _serverPrivacyPolicyInfo = null; + PrivacyPolicyInfo = null; _acceptPrivacyPolicyTcs = null; PrivacyPolicyDifferentVersion = default; } diff --git a/SS14.Launcher/Models/Data/DataManager.cs b/SS14.Launcher/Models/Data/DataManager.cs index 4e62ce933..7f5f26eaf 100644 --- a/SS14.Launcher/Models/Data/DataManager.cs +++ b/SS14.Launcher/Models/Data/DataManager.cs @@ -13,8 +13,8 @@ using DynamicData; using JetBrains.Annotations; using Microsoft.Data.Sqlite; +using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.Toolkit.Mvvm.Messaging; -using ReactiveUI; using Serilog; using SS14.Launcher.Utility; @@ -37,7 +37,7 @@ public interface ICVarEntry : INotifyPropertyChanged /// All data is stored in an SQLite DB. Simple config variables are stored K/V in a single table. /// More complex things like logins is stored in individual tables. /// -public sealed class DataManager : ReactiveObject +public sealed class DataManager : ObservableObject { private delegate void DbCommand(SqliteConnection connection); diff --git a/SS14.Launcher/Models/Data/FavoriteServer.cs b/SS14.Launcher/Models/Data/FavoriteServer.cs index eca1fa16a..8e69b63dd 100644 --- a/SS14.Launcher/Models/Data/FavoriteServer.cs +++ b/SS14.Launcher/Models/Data/FavoriteServer.cs @@ -1,19 +1,19 @@ using System; -using System.Text.Json.Serialization; -using ReactiveUI; +using Microsoft.Toolkit.Mvvm.ComponentModel; namespace SS14.Launcher.Models.Data; -public sealed class FavoriteServer : ReactiveObject +public sealed partial class FavoriteServer : ObservableObject { - private string? _name; - private DateTimeOffset _raiseTime; + public readonly string? Name; - // For serialization. - public FavoriteServer() - { - Address = default!; - } + public readonly string Address; + + /// + /// Used to infer an exact ordering for servers in a simple, compatible manner. + /// Defaults to 0, this is fine. + /// + [ObservableProperty] private DateTimeOffset _raiseTime; public FavoriteServer(string? name, string address) { @@ -27,25 +27,4 @@ public FavoriteServer(string? name, string address, DateTimeOffset raiseTime) Address = address; RaiseTime = raiseTime; } - - [JsonPropertyName("name")] - public string? Name - { - get => _name; - set => this.RaiseAndSetIfChanged(ref _name, value); - } - - [JsonPropertyName("address")] - public string Address { get; private set; } // Need private set for JSON.NET to work. - - /// - /// Used to infer an exact ordering for servers in a simple, compatible manner. - /// Defaults to 0, this is fine. - /// This isn't saved in JSON because the launcher apparently doesn't use JSON for these anymore. - /// - public DateTimeOffset RaiseTime - { - get => _raiseTime; - set => this.RaiseAndSetIfChanged(ref _raiseTime, value); - } } diff --git a/SS14.Launcher/Models/Data/LoginInfo.cs b/SS14.Launcher/Models/Data/LoginInfo.cs index ccd43f5bc..7f39b6f31 100644 --- a/SS14.Launcher/Models/Data/LoginInfo.cs +++ b/SS14.Launcher/Models/Data/LoginInfo.cs @@ -1,17 +1,13 @@ using System; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; +using Microsoft.Toolkit.Mvvm.ComponentModel; namespace SS14.Launcher.Models.Data; -public class LoginInfo : ReactiveObject +public partial class LoginInfo : ObservableObject { - [Reactive] - public Guid UserId { get; set; } - [Reactive] - public string Username { get; set; } = default!; - [Reactive] - public LoginToken Token { get; set; } + public Guid UserId; + public string? Username; + [ObservableProperty] private LoginToken _token; public override string ToString() { diff --git a/SS14.Launcher/Models/Logins/LoggedInAccount.cs b/SS14.Launcher/Models/Logins/LoggedInAccount.cs index c5724116a..2ba0861e3 100644 --- a/SS14.Launcher/Models/Logins/LoggedInAccount.cs +++ b/SS14.Launcher/Models/Logins/LoggedInAccount.cs @@ -1,10 +1,10 @@ using System; -using ReactiveUI; +using Microsoft.Toolkit.Mvvm.ComponentModel; using SS14.Launcher.Models.Data; namespace SS14.Launcher.Models.Logins; -public abstract class LoggedInAccount : ReactiveObject +public abstract class LoggedInAccount : ObservableObject { public string Username => LoginInfo.Username; public Guid UserId => LoginInfo.UserId; @@ -17,4 +17,4 @@ protected LoggedInAccount(LoginInfo loginInfo) public LoginInfo LoginInfo { get; } public abstract AccountLoginStatus Status { get; } -} \ No newline at end of file +} diff --git a/SS14.Launcher/Models/Logins/LoginManager.cs b/SS14.Launcher/Models/Logins/LoginManager.cs index 963c5b52a..2f1785263 100644 --- a/SS14.Launcher/Models/Logins/LoginManager.cs +++ b/SS14.Launcher/Models/Logins/LoginManager.cs @@ -1,10 +1,9 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Avalonia.Threading; using DynamicData; -using ReactiveUI; +using Microsoft.Toolkit.Mvvm.ComponentModel; using Serilog; using SS14.Launcher.Api; using SS14.Launcher.Models.Data; @@ -13,7 +12,7 @@ namespace SS14.Launcher.Models.Logins; // This is different from DataManager in that this class actually manages logic more complex than raw storage. // Checking and refreshing tokens, marking accounts as "need signing in again", etc... -public sealed class LoginManager : ReactiveObject +public sealed class LoginManager : ObservableObject { // TODO: If the user tries to connect to a server or such // on the split second interval that the launcher does a token refresh @@ -46,8 +45,14 @@ public Guid? ActiveAccountId } } - this.RaiseAndSetIfChanged(ref _activeLoginId, value); - this.RaisePropertyChanged(nameof(ActiveAccount)); + if (_activeLoginId != value) + { + OnPropertyChanging(); + _activeLoginId = value; + OnPropertyChanged(); + } + + OnPropertyChanged(nameof(ActiveAccount)); _cfg.SelectedLoginId = value; } } @@ -86,7 +91,7 @@ public LoginManager(DataManager cfg, AuthApi authApi) public async Task Initialize() { // Set up timer so that if the user leaves their launcher open for a month or something - // his tokens don't expire. + // their tokens don't expire. _timer = DispatcherTimer.Run(() => { async void Impl() @@ -203,7 +208,12 @@ public ActiveLoginData(LoginInfo info) : base(info) public void SetStatus(AccountLoginStatus status) { - this.RaiseAndSetIfChanged(ref _status, status, nameof(Status)); + if (_status == status) + return; + + OnPropertyChanging(nameof(Status)); + _status = status; + OnPropertyChanged(nameof(Status)); Log.Debug("Setting status for login {account} to {status}", LoginInfo, status); } } diff --git a/SS14.Launcher/Models/ServerStatus/ServerListCache.cs b/SS14.Launcher/Models/ServerStatus/ServerListCache.cs index 112c6fe74..5e611f51d 100644 --- a/SS14.Launcher/Models/ServerStatus/ServerListCache.cs +++ b/SS14.Launcher/Models/ServerStatus/ServerListCache.cs @@ -1,15 +1,12 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Collections.Specialized; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Toolkit.Mvvm.ComponentModel; using Serilog; using Splat; using SS14.Launcher.Utility; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; using SS14.Launcher.Api; using SS14.Launcher.Models.Data; using static SS14.Launcher.Api.HubApi; @@ -19,24 +16,16 @@ namespace SS14.Launcher.Models.ServerStatus; /// /// Caches the Hub's server list. /// -public sealed class ServerListCache : ReactiveObject, IServerSource +public sealed partial class ServerListCache : ObservableObject, IServerSource { - private readonly HubApi _hubApi; - private readonly DataManager _dataManager; + private readonly HubApi _hubApi = Locator.Current.GetRequiredService(); + private readonly DataManager _dataManager = Locator.Current.GetRequiredService(); private CancellationTokenSource? _refreshCancel; - public ObservableCollection AllServers => _allServers; - private readonly ServerListCollection _allServers = new(); + public ObservableList AllServers { get; } = []; - [Reactive] - public RefreshListStatus Status { get; private set; } = RefreshListStatus.NotUpdated; - - public ServerListCache() - { - _hubApi = Locator.Current.GetRequiredService(); - _dataManager = Locator.Current.GetRequiredService(); - } + [ObservableProperty] private RefreshListStatus _status = RefreshListStatus.NotUpdated; /// /// This function requests the initial update from the server if one hasn't already been requested. @@ -55,14 +44,14 @@ public void RequestInitialUpdate() public void RequestRefresh() { _refreshCancel?.Cancel(); - _allServers.Clear(); + AllServers.Clear(); _refreshCancel = new CancellationTokenSource(10000); RefreshServerList(_refreshCancel.Token); } public async void RefreshServerList(CancellationToken cancel) { - _allServers.Clear(); + AllServers.Clear(); Status = RefreshListStatus.UpdatingMaster; try @@ -129,14 +118,14 @@ public async void RefreshServerList(CancellationToken cancel) } } - _allServers.AddItems(entries.Select(entry => + AllServers.AddRange(entries.Select(entry => { var statusData = new ServerStatusData(entry.Address, entry.HubAddress); ServerStatusCache.ApplyStatus(statusData, entry.StatusData); return statusData; })); - if (_allServers.Count == 0) + if (AllServers.Count == 0) // We did not get any servers Status = RefreshListStatus.Error; else if (!allSucceeded) @@ -167,19 +156,6 @@ void IServerSource.UpdateInfoFor(ServerStatusData statusData) statusData, async token => await _hubApi.GetServerInfo(statusData.Address, statusData.HubAddress, token)); } - - private sealed class ServerListCollection : ObservableCollection - { - public void AddItems(IEnumerable items) - { - foreach (var item in items) - { - Items.Add(item); - } - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - } } public class ServerStatusDataWithFallbackName diff --git a/SS14.Launcher/Models/Updater.cs b/SS14.Launcher/Models/Updater.cs index 01b674f74..73baae655 100644 --- a/SS14.Launcher/Models/Updater.cs +++ b/SS14.Launcher/Models/Updater.cs @@ -14,8 +14,7 @@ using Avalonia.Threading; using Dapper; using Microsoft.Data.Sqlite; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; +using Microsoft.Toolkit.Mvvm.ComponentModel; using Serilog; using Splat; using SS14.Launcher.Models.ContentManagement; @@ -27,7 +26,7 @@ namespace SS14.Launcher.Models; -public sealed partial class Updater : ReactiveObject +public sealed partial class Updater : ObservableObject { private const int ManifestDownloadProtocolVersion = 1; @@ -53,9 +52,9 @@ public Updater() } // Note: these get updated from different threads. Observe responsibly. - [Reactive] public UpdateStatus Status { get; private set; } - [Reactive] public (long downloaded, long total, ProgressUnit unit)? Progress { get; private set; } - [Reactive] public long? Speed { get; private set; } + [ObservableProperty] private UpdateStatus _status; + [ObservableProperty] private (long downloaded, long total, ProgressUnit unit)? _progress; + [ObservableProperty] private long? _speed; public Exception? UpdateException; @@ -377,10 +376,10 @@ private void CullOldContentVersions(SqliteConnection con) if (anythingRemoved) { var rows = con.Execute(""" - DELETE FROM Content - WHERE Id NOT IN (SELECT ContentId FROM ContentManifest) - AND Id NOT IN (SELECT ContentId FROM InterruptedDownloadContent) - """); + DELETE FROM Content + WHERE Id NOT IN (SELECT ContentId FROM ContentManifest) + AND Id NOT IN (SELECT ContentId FROM InterruptedDownloadContent) + """); Log.Debug("Culled {RowsCulled} orphaned content blobs", rows); } @@ -526,17 +525,17 @@ private async Task TouchOrDownloadContentUpdateTransacted( private static void SaveInterruptedDownload(SqliteConnection con, TransactedDownloadState state) { var interruptedId = con.ExecuteScalar(""" - INSERT INTO InterruptedDownload (Added) - VALUES (datetime('now')) - RETURNING Id - """); + INSERT INTO InterruptedDownload (Added) + VALUES (datetime('now')) + RETURNING Id + """); foreach (var contentId in state.DownloadedContentEntries) { con.Execute(""" - INSERT INTO InterruptedDownloadContent(InterruptedDownloadId, ContentId) - VALUES (@DownloadId, @ContentId) - """, + INSERT INTO InterruptedDownloadContent(InterruptedDownloadId, ContentId) + VALUES (@DownloadId, @ContentId) + """, new { DownloadId = interruptedId, ContentId = contentId }); } } diff --git a/SS14.Launcher/Program.cs b/SS14.Launcher/Program.cs index 0934225a9..7fdbbcfb5 100644 --- a/SS14.Launcher/Program.cs +++ b/SS14.Launcher/Program.cs @@ -9,7 +9,6 @@ using Avalonia; using Avalonia.Logging; using Avalonia.Media; -using Avalonia.ReactiveUI; using Microsoft.Win32; using Serilog; using Serilog.Sinks.SystemConsole.Themes; @@ -245,8 +244,7 @@ private static AppBuilder BuildAvaloniaApp(DataManager cfg) { // Necessary workaround for #84 on Linux DefaultFamilyName = "avares://SS14.Launcher/Assets/Fonts/noto_sans/*.ttf#Noto Sans" - }) - .UseReactiveUI(); + }); } private static void CheckLauncherArchitecture(DataManager cfg, EngineManagerDynamic engineManager) diff --git a/SS14.Launcher/SS14.Launcher.csproj b/SS14.Launcher/SS14.Launcher.csproj index 9800cf9d0..36df92cf7 100644 --- a/SS14.Launcher/SS14.Launcher.csproj +++ b/SS14.Launcher/SS14.Launcher.csproj @@ -40,7 +40,6 @@ - @@ -48,11 +47,10 @@ + - - diff --git a/SS14.Launcher/Utility/ObservableList.cs b/SS14.Launcher/Utility/ObservableList.cs new file mode 100644 index 000000000..3ec4703f5 --- /dev/null +++ b/SS14.Launcher/Utility/ObservableList.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace SS14.Launcher.Utility; + +/// +/// Represents a dynamic data collection that provides notifications when items get added or removed, or when the whole +/// list is refreshed. +/// +/// Unlike , also exposes range operations. These +/// only trigger events once per range operation. +/// +/// The type of elements in the collection. +public class ObservableList : ObservableCollection +{ + /// + /// Initializes a new instance of the class that contains elements copied from the + /// specified collection. + /// + /// The collection from which the elements are copied. + public ObservableList(IEnumerable collection) : base(collection) + { + } + + /// + /// Initializes a new instance of the class. + /// + public ObservableList() : base() + { + } + + /// + /// Replace all elements in the with the elements of the specified collection. + /// + public void SetItems(IEnumerable collection) + { + Items.Clear(); + + foreach (var item in collection) + { + Items.Add(item); + } + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + /// + /// Adds the elements of the specified collection to the end of the . + /// + public void AddRange(IEnumerable collection) + { + foreach (var item in collection) + { + Items.Add(item); + } + + new List().AddRange(new List()); + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } +} diff --git a/SS14.Launcher/ViewModels/AccountDropDownViewModel.cs b/SS14.Launcher/ViewModels/AccountDropDownViewModel.cs index f5544c78e..4a205cdb5 100644 --- a/SS14.Launcher/ViewModels/AccountDropDownViewModel.cs +++ b/SS14.Launcher/ViewModels/AccountDropDownViewModel.cs @@ -1,10 +1,8 @@ using System; -using System.Collections.ObjectModel; -using System.Reactive.Linq; -using DynamicData; +using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; +using Microsoft.Toolkit.Mvvm.ComponentModel; using Serilog; using Splat; using SS14.Launcher.Api; @@ -15,16 +13,15 @@ namespace SS14.Launcher.ViewModels; -public class AccountDropDownViewModel : ViewModelBase +public partial class AccountDropDownViewModel : ViewModelBase { private readonly MainWindowViewModel _mainVm; private readonly DataManager _cfg; private readonly AuthApi _authApi; private readonly LoginManager _loginMgr; - private readonly ReadOnlyObservableCollection _accounts; private readonly LocalizationManager _loc; - public ReadOnlyObservableCollection Accounts => _accounts; + private ObservableList Accounts { get; } public bool EnableMultiAccounts => _cfg.ActuallyMultiAccounts; @@ -36,36 +33,35 @@ public AccountDropDownViewModel(MainWindowViewModel mainVm) _loginMgr = Locator.Current.GetRequiredService(); _loc = LocalizationManager.Instance; - this.WhenAnyValue(x => x._loginMgr.ActiveAccount) - .Subscribe(_ => - { - this.RaisePropertyChanged(nameof(LoginText)); - this.RaisePropertyChanged(nameof(AccountSwitchText)); - this.RaisePropertyChanged(nameof(LogoutText)); - this.RaisePropertyChanged(nameof(AccountControlsVisible)); - this.RaisePropertyChanged(nameof(AccountSwitchVisible)); - }); + Accounts = new(GetInactiveAccounts()); - _loginMgr.Logins.Connect().Subscribe(_ => + _loginMgr.PropertyChanged += (_, e) => { - this.RaisePropertyChanged(nameof(LogoutText)); - this.RaisePropertyChanged(nameof(AccountSwitchVisible)); - }); + if (e.PropertyName is not nameof(_loginMgr.ActiveAccount)) + return; - var filterObservable = this.WhenAnyValue(x => x._loginMgr.ActiveAccount) - .Select(MakeFilter); + OnPropertyChanged(nameof(LoginText)); + OnPropertyChanged(nameof(AccountSwitchText)); + OnPropertyChanged(nameof(LogoutText)); + OnPropertyChanged(nameof(AccountControlsVisible)); + OnPropertyChanged(nameof(AccountSwitchVisible)); - _loginMgr.Logins - .Connect() - .Filter(filterObservable) - .Transform(p => new AvailableAccountViewModel(p)) - .Bind(out _accounts) - .Subscribe(); + Accounts.SetItems(GetInactiveAccounts()); + }; + + _loginMgr.Logins.Connect().Subscribe(_ => + { + OnPropertyChanged(nameof(LogoutText)); + OnPropertyChanged(nameof(AccountSwitchVisible)); + }); } - private static Func MakeFilter(LoggedInAccount? selected) + private IEnumerable GetInactiveAccounts() { - return l => l != selected; + return _loginMgr.Logins + .Items + .Where(a => a != _loginMgr.ActiveAccount) + .Select(a => new AvailableAccountViewModel(a)); } public string LoginText => _loginMgr.ActiveAccount?.Username ?? @@ -82,7 +78,7 @@ public AccountDropDownViewModel(MainWindowViewModel mainVm) public bool AccountControlsVisible => _loginMgr.ActiveAccount != null; - [Reactive] public bool IsDropDownOpen { get; set; } + [ObservableProperty] private bool _isDropDownOpen; public async void LogoutPressed() { @@ -116,23 +112,20 @@ public void AddAccountPressed() } } -public sealed class AvailableAccountViewModel : ViewModelBase +public sealed partial class AvailableAccountViewModel : ViewModelBase { - public extern string StatusText { [ObservableAsProperty] get; } + [ObservableProperty] private LoggedInAccount _account; - public LoggedInAccount Account { get; } + public string StatusText + => Account.Username + Account.Status switch + { + AccountLoginStatus.Available => "", + AccountLoginStatus.Expired => " (!)", + _ => " (?)", + }; public AvailableAccountViewModel(LoggedInAccount account) { Account = account; - - this.WhenAnyValue(p => p.Account.Status, p => p.Account.Username) - .Select(p => p.Item1 switch - { - AccountLoginStatus.Available => $"{p.Item2}", - AccountLoginStatus.Expired => $"{p.Item2} (!)", - _ => $"{p.Item2} (?)" - }) - .ToPropertyEx(this, x => x.StatusText); } } diff --git a/SS14.Launcher/ViewModels/ConnectingViewModel.cs b/SS14.Launcher/ViewModels/ConnectingViewModel.cs index d25a36b72..c8f6489e6 100644 --- a/SS14.Launcher/ViewModels/ConnectingViewModel.cs +++ b/SS14.Launcher/ViewModels/ConnectingViewModel.cs @@ -1,12 +1,11 @@ using System; -using System.Reactive.Linq; using System.Threading; using Avalonia.Platform.Storage; -using ReactiveUI; using Splat; using SS14.Launcher.Localization; using SS14.Launcher.Models; using SS14.Launcher.Utility; +using static SS14.Launcher.Models.Connector.ConnectionStatus; namespace SS14.Launcher.ViewModels; @@ -22,16 +21,11 @@ public class ConnectingViewModel : ViewModelBase private string? _reasonSuffix; - private Connector.ConnectionStatus _connectorStatus; - private Updater.UpdateStatus _updaterStatus; - private (long downloaded, long total, Updater.ProgressUnit unit)? _updaterProgress; - private long? _updaterSpeed; - - public bool IsErrored => _connectorStatus == Connector.ConnectionStatus.ConnectionFailed || - _connectorStatus == Connector.ConnectionStatus.UpdateError || - _connectorStatus == Connector.ConnectionStatus.NotAContentBundle || - _connectorStatus == Connector.ConnectionStatus.ClientExited && - _connector.ClientExitedBadly; + public bool IsErrored + => _connector.Status == ConnectionFailed || + _connector.Status == UpdateError || + _connector.Status == NotAContentBundle || + _connector is { Status: ClientExited, ClientExitedBadly: true }; public static event Action? StartedConnecting; @@ -44,81 +38,67 @@ public ConnectingViewModel(Connector connector, MainWindowViewModel windowVm, st _connectionType = connectionType; _reasonSuffix = (givenReason != null) ? ("\n" + givenReason) : ""; - this.WhenAnyValue(x => x._updater.Progress) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(progress => - { - _updaterProgress = progress; - - this.RaisePropertyChanged(nameof(Progress)); - this.RaisePropertyChanged(nameof(ProgressIndeterminate)); - this.RaisePropertyChanged(nameof(ProgressText)); - }); - - this.WhenAnyValue(x => x._updater.Speed) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(speed => - { - _updaterSpeed = speed; - - this.RaisePropertyChanged(nameof(SpeedText)); - this.RaisePropertyChanged(nameof(SpeedIndeterminate)); - }); - - this.WhenAnyValue(x => x._updater.Status) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(status => - { - _updaterStatus = status; - this.RaisePropertyChanged(nameof(StatusText)); - }); - - this.WhenAnyValue(x => x._connector.Status) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(val => - { - _connectorStatus = val; - - this.RaisePropertyChanged(nameof(ProgressIndeterminate)); - this.RaisePropertyChanged(nameof(StatusText)); - this.RaisePropertyChanged(nameof(ProgressBarVisible)); - this.RaisePropertyChanged(nameof(IsErrored)); - this.RaisePropertyChanged(nameof(IsAskingPrivacyPolicy)); - - if (val == Connector.ConnectionStatus.ClientRunning - || val == Connector.ConnectionStatus.Cancelled - || val == Connector.ConnectionStatus.ClientExited && !_connector.ClientExitedBadly) - { - CloseOverlay(); - } - }); - - this.WhenAnyValue(x => x._connector.PrivacyPolicyDifferentVersion) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(_ => + _updater.PropertyChanged += (_, e) => + { + switch (e.PropertyName) { - this.RaisePropertyChanged(nameof(PrivacyPolicyText)); - }); + case nameof(_updater.Progress): + OnPropertyChanged(nameof(Progress)); + OnPropertyChanged(nameof(ProgressIndeterminate)); + OnPropertyChanged(nameof(ProgressText)); + break; + + case nameof(_updater.Speed): + OnPropertyChanged(nameof(SpeedText)); + OnPropertyChanged(nameof(SpeedIndeterminate)); + break; + + case nameof(_updater.Status): + OnPropertyChanged(nameof(StatusText)); + break; + } + }; - this.WhenAnyValue(x => x._connector.ClientExitedBadly) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(_ => + _connector.PropertyChanged += (_, e) => + { + switch (e.PropertyName) { - this.RaisePropertyChanged(nameof(StatusText)); - this.RaisePropertyChanged(nameof(IsErrored)); - }); + case nameof(_connector.Status): + OnPropertyChanged(nameof(ProgressIndeterminate)); + OnPropertyChanged(nameof(StatusText)); + OnPropertyChanged(nameof(ProgressBarVisible)); + OnPropertyChanged(nameof(IsErrored)); + OnPropertyChanged(nameof(IsAskingPrivacyPolicy)); + + if (_connector.Status == ClientRunning + || _connector.Status == Cancelled + || _connector is { Status: ClientExited, ClientExitedBadly: false }) + { + CloseOverlay(); + } + + break; + case nameof(_connector.PrivacyPolicyDifferentVersion): + OnPropertyChanged(nameof(PrivacyPolicyText)); + break; + case nameof(_connector.ClientExitedBadly): + OnPropertyChanged(nameof(StatusText)); + OnPropertyChanged(nameof(IsErrored)); + break; + } + }; } public float Progress { get { - if (_updaterProgress == null) + if (_updater.Progress == null) { return 0; } - var (downloaded, total, _) = _updaterProgress.Value; + var (downloaded, total, _) = _updater.Progress.Value; return downloaded / (float)total; } @@ -128,12 +108,12 @@ public string ProgressText { get { - if (_updaterProgress == null) + if (_updater.Progress == null) { return ""; } - var (downloaded, total, unit) = _updaterProgress.Value; + var (downloaded, total, unit) = _updater.Progress.Value; return unit switch { @@ -143,34 +123,36 @@ public string ProgressText } } - public bool ProgressIndeterminate => _connectorStatus != Connector.ConnectionStatus.Updating - || _updaterProgress == null; + public bool ProgressIndeterminate + => _connector.Status != Updating + || _updater.Progress == null; - public bool ProgressBarVisible => _connectorStatus != Connector.ConnectionStatus.ClientExited && - _connectorStatus != Connector.ConnectionStatus.ClientRunning && - _connectorStatus != Connector.ConnectionStatus.ConnectionFailed && - _connectorStatus != Connector.ConnectionStatus.UpdateError && - _connectorStatus != Connector.ConnectionStatus.NotAContentBundle; + public bool ProgressBarVisible + => _connector.Status != ClientExited && + _connector.Status != ClientRunning && + _connector.Status != ConnectionFailed && + _connector.Status != UpdateError && + _connector.Status != NotAContentBundle; - public bool SpeedIndeterminate => _connectorStatus != Connector.ConnectionStatus.Updating || _updaterSpeed == null; + public bool SpeedIndeterminate => _connector.Status != Updating || _updater.Speed == null; public string SpeedText { get { - if (_updaterSpeed is not { } speed) + if (_updater.Speed is not { } speed) return ""; return $"{Helpers.FormatBytes(speed)}/s"; } } - public string StatusText => - _connectorStatus switch + public string StatusText + => _connector.Status switch { - Connector.ConnectionStatus.None => _loc.GetString("connecting-status-none") + _reasonSuffix, - Connector.ConnectionStatus.UpdateError => FormatUpdateError(), - Connector.ConnectionStatus.Updating => _loc.GetString("connecting-status-updating", ("status", _loc.GetString(_updaterStatus switch + None => _loc.GetString("connecting-status-none") + _reasonSuffix, + UpdateError => FormatUpdateError(), + Updating => _loc.GetString("connecting-status-updating", ("status", _loc.GetString(_updater.Status switch { Updater.UpdateStatus.CheckingClientUpdate => "connecting-update-status-checking-client-update", Updater.UpdateStatus.DownloadingEngineVersion => "connecting-update-status-downloading-engine", @@ -187,11 +169,11 @@ public string SpeedText Updater.UpdateStatus.LoadingContentBundle => "connecting-update-status-loading-content-bundle", _ => "connecting-update-status-unknown" }))) + _reasonSuffix, - Connector.ConnectionStatus.Connecting => _loc.GetString("connecting-status-connecting") + _reasonSuffix, - Connector.ConnectionStatus.ConnectionFailed => _loc.GetString("connecting-status-connection-failed"), - Connector.ConnectionStatus.StartingClient => _loc.GetString("connecting-status-starting-client") + _reasonSuffix, - Connector.ConnectionStatus.NotAContentBundle => _loc.GetString("connecting-status-not-a-content-bundle"), - Connector.ConnectionStatus.ClientExited => _connector.ClientExitedBadly + Connecting => _loc.GetString("connecting-status-connecting") + _reasonSuffix, + ConnectionFailed => _loc.GetString("connecting-status-connection-failed"), + StartingClient => _loc.GetString("connecting-status-starting-client") + _reasonSuffix, + NotAContentBundle => _loc.GetString("connecting-status-not-a-content-bundle"), + ClientExited => _connector.ClientExitedBadly ? _loc.GetString("connecting-status-client-crashed") : "", _ => "" @@ -215,7 +197,7 @@ private string FormatUpdateError() _ => "" }; - public bool IsAskingPrivacyPolicy => _connectorStatus == Connector.ConnectionStatus.AwaitingPrivacyPolicyAcceptance; + public bool IsAskingPrivacyPolicy => _connector.Status == AwaitingPrivacyPolicyAcceptance; public string PrivacyPolicyText => _connector.PrivacyPolicyDifferentVersion ? _loc.GetString("connecting-privacy-policy-text-version-changed") diff --git a/SS14.Launcher/ViewModels/HubSettingsViewModel.cs b/SS14.Launcher/ViewModels/HubSettingsViewModel.cs index 88f6bf606..3d9b00d4d 100644 --- a/SS14.Launcher/ViewModels/HubSettingsViewModel.cs +++ b/SS14.Launcher/ViewModels/HubSettingsViewModel.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; -using DynamicData; using Splat; using SS14.Launcher.Models.Data; using SS14.Launcher.Utility; @@ -13,7 +11,7 @@ namespace SS14.Launcher.ViewModels; public class HubSettingsViewModel : ViewModelBase { public string[] DefaultHubs => ConfigConstants.DefaultHubUrls.Select(set => set.Urls[0]).ToArray(); - public ObservableCollection HubList { get; set; } = new(); + public ObservableList HubList { get; set; } = new(); private readonly DataManager _dataManager = Locator.Current.GetRequiredService(); diff --git a/SS14.Launcher/ViewModels/Login/AuthTfaViewModel.cs b/SS14.Launcher/ViewModels/Login/AuthTfaViewModel.cs index 294f90c7d..221744d12 100644 --- a/SS14.Launcher/ViewModels/Login/AuthTfaViewModel.cs +++ b/SS14.Launcher/ViewModels/Login/AuthTfaViewModel.cs @@ -1,22 +1,20 @@ using System; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; +using Microsoft.Toolkit.Mvvm.ComponentModel; using SS14.Launcher.Api; using SS14.Launcher.Models.Data; using SS14.Launcher.Models.Logins; namespace SS14.Launcher.ViewModels.Login; -public sealed class AuthTfaViewModel : BaseLoginViewModel +public sealed partial class AuthTfaViewModel : BaseLoginViewModel { private readonly AuthApi.AuthenticateRequest _request; private readonly LoginManager _loginMgr; private readonly AuthApi _authApi; private readonly DataManager _cfg; - [Reactive] public string Code { get; set; } = ""; - - [Reactive] public bool IsInputValid { get; private set; } + [ObservableProperty] private string _code = ""; + [ObservableProperty] private bool _isInputValid; public AuthTfaViewModel( MainWindowLoginViewModel parentVm, @@ -30,13 +28,16 @@ public AuthTfaViewModel( _authApi = authApi; _cfg = cfg; - this.WhenAnyValue(x => x.Code) - .Subscribe(s => { IsInputValid = CheckInputValid(s); }); + PropertyChanged += (_, e) => + { + if (e.PropertyName is nameof(Code)) + IsInputValid = CheckInputValid(); + }; } - private static bool CheckInputValid(string code) + private bool CheckInputValid() { - var trimmed = code.AsSpan().Trim(); + var trimmed = Code.AsSpan().Trim(); if (trimmed.Length != 6) return false; diff --git a/SS14.Launcher/ViewModels/Login/BaseLoginViewModel.cs b/SS14.Launcher/ViewModels/Login/BaseLoginViewModel.cs index 1d4c817d3..15d5dbd13 100644 --- a/SS14.Launcher/ViewModels/Login/BaseLoginViewModel.cs +++ b/SS14.Launcher/ViewModels/Login/BaseLoginViewModel.cs @@ -1,22 +1,16 @@ -using ReactiveUI.Fody.Helpers; +using Microsoft.Toolkit.Mvvm.ComponentModel; namespace SS14.Launcher.ViewModels.Login; -public abstract class BaseLoginViewModel : ViewModelBase, IErrorOverlayOwner +public abstract partial class BaseLoginViewModel(MainWindowLoginViewModel parentVM) : ViewModelBase, IErrorOverlayOwner { - [Reactive] public bool Busy { get; protected set; } - [Reactive] public string? BusyText { get; protected set; } - [Reactive] public ViewModelBase? OverlayControl { get; set; } - public MainWindowLoginViewModel ParentVM { get; } - - protected BaseLoginViewModel(MainWindowLoginViewModel parentVM) - { - ParentVM = parentVM; - } + [ObservableProperty] private bool _busy; + [ObservableProperty] private string? _busyText; + [ObservableProperty] private ViewModelBase? _overlayControl; + public MainWindowLoginViewModel ParentVM { get; } = parentVM; public virtual void Activated() { - } public virtual void OverlayOk() diff --git a/SS14.Launcher/ViewModels/Login/ExpiredLoginViewModel.cs b/SS14.Launcher/ViewModels/Login/ExpiredLoginViewModel.cs index 19e0e0c39..6b9c3466f 100644 --- a/SS14.Launcher/ViewModels/Login/ExpiredLoginViewModel.cs +++ b/SS14.Launcher/ViewModels/Login/ExpiredLoginViewModel.cs @@ -1,32 +1,20 @@ -using ReactiveUI.Fody.Helpers; +using Microsoft.Toolkit.Mvvm.ComponentModel; using SS14.Launcher.Api; using SS14.Launcher.Models.Data; using SS14.Launcher.Models.Logins; namespace SS14.Launcher.ViewModels.Login; -public class ExpiredLoginViewModel : BaseLoginViewModel +public partial class ExpiredLoginViewModel ( + MainWindowLoginViewModel parentVm, + DataManager cfg, + AuthApi authApi, + LoginManager loginMgr, + LoggedInAccount account) + : BaseLoginViewModel(parentVm) { - private readonly DataManager _cfg; - private readonly AuthApi _authApi; - private readonly LoginManager _loginMgr; - - public ExpiredLoginViewModel( - MainWindowLoginViewModel parentVm, - DataManager cfg, - AuthApi authApi, - LoginManager loginMgr, - LoggedInAccount account) - : base(parentVm) - { - _cfg = cfg; - _authApi = authApi; - _loginMgr = loginMgr; - Account = account; - } - - [Reactive] public string EditingPassword { get; set; } = ""; - public LoggedInAccount Account { get; } + [ObservableProperty] private string _password = ""; + public LoggedInAccount Account { get; } = account; public async void OnLogInButtonPressed() { @@ -36,12 +24,12 @@ public async void OnLogInButtonPressed() Busy = true; try { - var request = new AuthApi.AuthenticateRequest(Account.UserId, EditingPassword); - var resp = await _authApi.AuthenticateAsync(request); + var request = new AuthApi.AuthenticateRequest(Account.UserId, Password); + var resp = await authApi.AuthenticateAsync(request); - await LoginViewModel.DoLogin(this, request, resp, _loginMgr, _authApi); + await LoginViewModel.DoLogin(this, request, resp, loginMgr, authApi); - _cfg.CommitConfig(); + cfg.CommitConfig(); } finally { @@ -51,8 +39,8 @@ public async void OnLogInButtonPressed() public void OnLogOutButtonPressed() { - _cfg.RemoveLogin(Account.LoginInfo); - _cfg.CommitConfig(); + cfg.RemoveLogin(Account.LoginInfo); + cfg.CommitConfig(); ParentVM.SwitchToLogin(); } diff --git a/SS14.Launcher/ViewModels/Login/ForgotPasswordViewModel.cs b/SS14.Launcher/ViewModels/Login/ForgotPasswordViewModel.cs index 7ebe3ac28..9d27662fe 100644 --- a/SS14.Launcher/ViewModels/Login/ForgotPasswordViewModel.cs +++ b/SS14.Launcher/ViewModels/Login/ForgotPasswordViewModel.cs @@ -1,26 +1,16 @@ -using ReactiveUI.Fody.Helpers; +using Microsoft.Toolkit.Mvvm.ComponentModel; using SS14.Launcher.Api; using SS14.Launcher.Localization; namespace SS14.Launcher.ViewModels.Login; -public sealed class ForgotPasswordViewModel : BaseLoginViewModel +public sealed partial class ForgotPasswordViewModel(MainWindowLoginViewModel parentVM, AuthApi authApi) + : BaseLoginViewModel(parentVM) { - private readonly AuthApi _authApi; private readonly LocalizationManager _loc = LocalizationManager.Instance; - - [Reactive] public string EditingEmail { get; set; } = ""; - + [ObservableProperty] private string _email = ""; private bool _errored; - public ForgotPasswordViewModel( - MainWindowLoginViewModel parentVM, - AuthApi authApi) - : base(parentVM) - { - _authApi = authApi; - } - public async void SubmitPressed() { if (Busy) @@ -30,17 +20,16 @@ public async void SubmitPressed() try { BusyText = "Sending email..."; - var errors = await _authApi.ForgotPasswordAsync(EditingEmail); + var errors = await authApi.ForgotPasswordAsync(Email); _errored = errors != null; if (!_errored) { // This isn't an error lol but that's what I called the control. - OverlayControl = new AuthErrorsOverlayViewModel(this, _loc.GetString("login-forgot-success-title"), new[] - { - _loc.GetString("login-forgot-success-message") - }); + OverlayControl = new AuthErrorsOverlayViewModel(this, _loc.GetString("login-forgot-success-title"), [ + _loc.GetString("login-forgot-success-message"), + ]); } else { diff --git a/SS14.Launcher/ViewModels/Login/LoginViewModel.cs b/SS14.Launcher/ViewModels/Login/LoginViewModel.cs index a5e7ce605..768ecda45 100644 --- a/SS14.Launcher/ViewModels/Login/LoginViewModel.cs +++ b/SS14.Launcher/ViewModels/Login/LoginViewModel.cs @@ -1,7 +1,5 @@ -using System; using System.Threading.Tasks; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; +using Microsoft.Toolkit.Mvvm.ComponentModel; using SS14.Launcher.Api; using SS14.Launcher.Localization; using SS14.Launcher.Models.Data; @@ -9,18 +7,16 @@ namespace SS14.Launcher.ViewModels.Login; -public class LoginViewModel : BaseLoginViewModel +public partial class LoginViewModel : BaseLoginViewModel { private readonly AuthApi _authApi; private readonly LoginManager _loginMgr; private readonly DataManager _dataManager; private readonly LocalizationManager _loc = LocalizationManager.Instance; - [Reactive] public string EditingUsername { get; set; } = ""; - [Reactive] public string EditingPassword { get; set; } = ""; - - [Reactive] public bool IsInputValid { get; private set; } - [Reactive] public bool IsPasswordVisible { get; set; } + [ObservableProperty] private string _username = ""; + [ObservableProperty] private string _password = ""; + [ObservableProperty] private bool _isInputValid; public LoginViewModel(MainWindowLoginViewModel parentVm, AuthApi authApi, LoginManager loginMgr, DataManager dataManager) : base(parentVm) @@ -30,8 +26,16 @@ public LoginViewModel(MainWindowLoginViewModel parentVm, AuthApi authApi, _loginMgr = loginMgr; _dataManager = dataManager; - this.WhenAnyValue(x => x.EditingUsername, x => x.EditingPassword) - .Subscribe(s => { IsInputValid = !string.IsNullOrEmpty(s.Item1) && !string.IsNullOrEmpty(s.Item2); }); + PropertyChanged += (_, e) => + { + switch (e.PropertyName) + { + case nameof(Username): + case nameof(Password): + IsInputValid = !string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password); + break; + } + }; } public async void OnLogInButtonPressed() @@ -44,7 +48,7 @@ public async void OnLogInButtonPressed() Busy = true; try { - var request = new AuthApi.AuthenticateRequest(EditingUsername, EditingPassword); + var request = new AuthApi.AuthenticateRequest(Username, Password); var resp = await _authApi.AuthenticateAsync(request); await DoLogin(this, request, resp, _loginMgr, _authApi); diff --git a/SS14.Launcher/ViewModels/Login/RegisterNeedsConfirmationViewModel.cs b/SS14.Launcher/ViewModels/Login/RegisterNeedsConfirmationViewModel.cs deleted file mode 100644 index bcf1549ec..000000000 --- a/SS14.Launcher/ViewModels/Login/RegisterNeedsConfirmationViewModel.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using Avalonia.Threading; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using SS14.Launcher.Api; -using SS14.Launcher.Localization; -using SS14.Launcher.Models.Data; -using SS14.Launcher.Models.Logins; - -namespace SS14.Launcher.ViewModels.Login; - -public class RegisterNeedsConfirmationViewModel : BaseLoginViewModel -{ - private const int TimeoutSeconds = 5; - - private readonly AuthApi _authApi; - private readonly LocalizationManager _loc = LocalizationManager.Instance; - - private readonly string _loginUsername; - private readonly string _loginPassword; - private readonly LoginManager _loginMgr; - private readonly DataManager _dataManager; - - public bool ConfirmButtonEnabled => TimeoutSecondsLeft == 0; - - public string ConfirmButtonText - { - get - { - var text = _loc.GetString("login-confirmation-button-confirm"); - if (TimeoutSecondsLeft != 0) - { - text = $"{text} ({TimeoutSecondsLeft})"; - } - - return text; - } - } - - [Reactive] private int TimeoutSecondsLeft { get; set; } - - public RegisterNeedsConfirmationViewModel( - MainWindowLoginViewModel parentVm, - AuthApi authApi, string username, string password, LoginManager loginMgr, DataManager dataManager) - : base(parentVm) - { - BusyText = _loc.GetString("login-confirmation-busy"); - _authApi = authApi; - - _loginUsername = username; - _loginPassword = password; - _loginMgr = loginMgr; - _dataManager = dataManager; - - this.WhenAnyValue(p => p.TimeoutSecondsLeft) - .Subscribe(_ => - { - this.RaisePropertyChanged(nameof(ConfirmButtonText)); - this.RaisePropertyChanged(nameof(ConfirmButtonEnabled)); - }); - } - - public override void Activated() - { - TimeoutSecondsLeft = TimeoutSeconds; - DispatcherTimer.Run(TimerTick, TimeSpan.FromSeconds(1)); - } - - private bool TimerTick() - { - TimeoutSecondsLeft -= 1; - return TimeoutSecondsLeft != 0; - } - - public async void ConfirmButtonPressed() - { - if (Busy) - return; - - Busy = true; - - try - { - var request = new AuthApi.AuthenticateRequest(_loginUsername, _loginPassword); - var resp = await _authApi.AuthenticateAsync(request); - - await LoginViewModel.DoLogin(this, request, resp, _loginMgr, _authApi); - - _dataManager.CommitConfig(); - } - finally - { - Busy = false; - } - } -} diff --git a/SS14.Launcher/ViewModels/Login/RegisterViewModel.cs b/SS14.Launcher/ViewModels/Login/RegisterViewModel.cs deleted file mode 100644 index 25d2aac8e..000000000 --- a/SS14.Launcher/ViewModels/Login/RegisterViewModel.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Diagnostics; -using System.Net.Mail; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using Robust.Shared.AuthLib; -using SS14.Launcher.Api; -using SS14.Launcher.Models.Data; -using SS14.Launcher.Models.Logins; - -namespace SS14.Launcher.ViewModels.Login; - -public class RegisterViewModel : BaseLoginViewModel -{ - private readonly DataManager _cfg; - private readonly AuthApi _authApi; - private readonly LoginManager _loginMgr; - - [Reactive] public string EditingUsername { get; set; } = ""; - [Reactive] public string EditingPassword { get; set; } = ""; - [Reactive] public string EditingPasswordConfirm { get; set; } = ""; - [Reactive] public string EditingEmail { get; set; } = ""; - - [Reactive] public bool IsInputValid { get; private set; } - [Reactive] public string InvalidReason { get; private set; } = " "; - - [Reactive] public bool Is13OrOlder { get; set; } - - - public RegisterViewModel(MainWindowLoginViewModel parentVm, DataManager cfg, AuthApi authApi, LoginManager loginMgr) - : base(parentVm) - { - _cfg = cfg; - _authApi = authApi; - _loginMgr = loginMgr; - - this.WhenAnyValue(x => x.EditingUsername, x => x.EditingPassword, x => x.EditingPasswordConfirm, - x => x.EditingEmail, x => x.Is13OrOlder) - .Subscribe(UpdateInputValid); - } - - private void UpdateInputValid((string user, string pass, string passConfirm, string email, bool is13OrOlder) s) - { - var (user, pass, passConfirm, email, is13OrOlder) = s; - - IsInputValid = false; - if (!UsernameHelpers.IsNameValid(user, out var reason)) - { - InvalidReason = reason switch - { - UsernameHelpers.UsernameInvalidReason.Empty => "Username is empty", - UsernameHelpers.UsernameInvalidReason.TooLong => "Username is too long", - UsernameHelpers.UsernameInvalidReason.TooShort => "Username is too short", - UsernameHelpers.UsernameInvalidReason.InvalidCharacter => "Username contains an invalid character", - _ => "???" - }; - return; - } - - if (string.IsNullOrEmpty(email)) - { - InvalidReason = "Email is empty"; - return; - } - - if (!MailAddress.TryCreate(email, out _)) - { - InvalidReason = "Email is invalid"; - return; - } - - if (string.IsNullOrEmpty(pass)) - { - InvalidReason = "Password is empty"; - return; - } - - if (pass != passConfirm) - { - InvalidReason = "Confirm password does not match"; - return; - } - - if (!is13OrOlder) - { - InvalidReason = "You must be 13 or older"; - return; - } - - InvalidReason = " "; - IsInputValid = true; - } - - public async void OnRegisterInButtonPressed() - { - if (!IsInputValid || Busy) - { - return; - } - - BusyText = "Registering account..."; - Busy = true; - try - { - var result = await _authApi.RegisterAsync(EditingUsername, EditingEmail, EditingPassword); - if (!result.IsSuccess) - { - OverlayControl = new AuthErrorsOverlayViewModel(this, "Unable to register", result.Errors); - return; - } - - var status = result.Status; - if (status == RegisterResponseStatus.Registered) - { - BusyText = "Logging in..."; - // No confirmation needed, log in immediately. - var request = new AuthApi.AuthenticateRequest(EditingUsername, EditingPassword); - var resp = await _authApi.AuthenticateAsync(request); - - await LoginViewModel.DoLogin(this, request, resp, _loginMgr, _authApi); - - _cfg.CommitConfig(); - } - else - { - Debug.Assert(status == RegisterResponseStatus.RegisteredNeedConfirmation); - - ParentVM.SwitchToRegisterNeedsConfirmation(EditingUsername, EditingPassword); - } - } - finally - { - Busy = false; - } - } -} diff --git a/SS14.Launcher/ViewModels/Login/ResendConfirmationViewModel.cs b/SS14.Launcher/ViewModels/Login/ResendConfirmationViewModel.cs deleted file mode 100644 index 1c04a7567..000000000 --- a/SS14.Launcher/ViewModels/Login/ResendConfirmationViewModel.cs +++ /dev/null @@ -1,63 +0,0 @@ -using ReactiveUI.Fody.Helpers; -using SS14.Launcher.Api; - -namespace SS14.Launcher.ViewModels.Login; - -public class ResendConfirmationViewModel : BaseLoginViewModel -{ - private readonly AuthApi _authApi; - - [Reactive] public string EditingEmail { get; set; } = ""; - - private bool _errored; - - public ResendConfirmationViewModel(MainWindowLoginViewModel parentVM, AuthApi authApi) : base(parentVM) - { - _authApi = authApi; - } - - public async void SubmitPressed() - { - if (Busy) - return; - - Busy = true; - try - { - BusyText = "Resending email..."; - var errors = await _authApi.ResendConfirmationAsync(EditingEmail); - - _errored = errors != null; - - if (!_errored) - { - // This isn't an error lol but that's what I called the control. - OverlayControl = new AuthErrorsOverlayViewModel(this, "Confirmation email sent", new [] - { - "A confirmation email has been sent to your email address." - }); - } - else - { - OverlayControl = new AuthErrorsOverlayViewModel(this, "Error", errors!); - } - } - finally - { - Busy = false; - } - } - - public override void OverlayOk() - { - if (_errored) - { - base.OverlayOk(); - } - else - { - // If the overlay was a success overlay, switch back to login. - ParentVM.SwitchToLogin(); - } - } -} diff --git a/SS14.Launcher/ViewModels/MainWindowLoginViewModel.cs b/SS14.Launcher/ViewModels/MainWindowLoginViewModel.cs index 4400f8536..6f90a5908 100644 --- a/SS14.Launcher/ViewModels/MainWindowLoginViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowLoginViewModel.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using ReactiveUI; using Splat; using SS14.Launcher.Api; using SS14.Launcher.Models.Data; @@ -23,7 +22,13 @@ public BaseLoginViewModel Screen get => _screen; set { - this.RaiseAndSetIfChanged(ref _screen, value); + if (_screen == value) + return; + + OnPropertyChanging(); + _screen = value; + OnPropertyChanged(); + value.Activated(); } } @@ -48,11 +53,6 @@ public void SwitchToExpiredLogin(LoggedInAccount account) Screen = new ExpiredLoginViewModel(this, _cfg, _authApi, _loginMgr, account); } - public void SwitchToRegister() - { - Screen = new RegisterViewModel(this, _cfg, _authApi, _loginMgr); - } - public void SwitchToForgotPassword() { Screen = new ForgotPasswordViewModel(this, _authApi); @@ -63,16 +63,6 @@ public void SwitchToAuthTfa(AuthApi.AuthenticateRequest request) Screen = new AuthTfaViewModel(this, request, _loginMgr, _authApi, _cfg); } - public void SwitchToResendConfirmation() - { - Screen = new ResendConfirmationViewModel(this, _authApi); - } - - public void SwitchToRegisterNeedsConfirmation(string username, string password) - { - Screen = new RegisterNeedsConfirmationViewModel(this, _authApi, username, password, _loginMgr, _cfg); - } - public void OpenLogDirectory() { Process.Start(new ProcessStartInfo diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/DevelopmentTabViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/DevelopmentTabViewModel.cs index 3d06d516a..3f281b03c 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/DevelopmentTabViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/DevelopmentTabViewModel.cs @@ -1,5 +1,4 @@ -using ReactiveUI; -using Splat; +using Splat; using SS14.Launcher.Localization; using SS14.Launcher.Models.Data; using SS14.Launcher.Utility; @@ -9,50 +8,49 @@ namespace SS14.Launcher.ViewModels.MainWindowTabs; public sealed class DevelopmentTabViewModel : MainWindowTabViewModel { private readonly LocalizationManager _loc = LocalizationManager.Instance; - public DataManager Cfg { get; } + private readonly DataManager _cfg = Locator.Current.GetRequiredService(); public DevelopmentTabViewModel() { - Cfg = Locator.Current.GetRequiredService(); - // TODO: This sucks and leaks. - Cfg.GetCVarEntry(CVars.EngineOverrideEnabled).PropertyChanged += (sender, args) => + _cfg.GetCVarEntry(CVars.EngineOverrideEnabled).PropertyChanged += (_, _) => { - this.RaisePropertyChanged(nameof(Name)); + OnPropertyChanged(nameof(Name)); }; } - public override string Name => Cfg.GetCVar(CVars.EngineOverrideEnabled) + public override string Name + => _cfg.GetCVar(CVars.EngineOverrideEnabled) ? _loc.GetString("tab-development-title-override") : _loc.GetString("tab-development-title"); public bool DisableSigning { - get => Cfg.GetCVar(CVars.DisableSigning); + get => _cfg.GetCVar(CVars.DisableSigning); set { - Cfg.SetCVar(CVars.DisableSigning, value); - Cfg.CommitConfig(); + _cfg.SetCVar(CVars.DisableSigning, value); + _cfg.CommitConfig(); } } public bool EngineOverrideEnabled { - get => Cfg.GetCVar(CVars.EngineOverrideEnabled); + get => _cfg.GetCVar(CVars.EngineOverrideEnabled); set { - Cfg.SetCVar(CVars.EngineOverrideEnabled, value); - Cfg.CommitConfig(); + _cfg.SetCVar(CVars.EngineOverrideEnabled, value); + _cfg.CommitConfig(); } } public string EngineOverridePath { - get => Cfg.GetCVar(CVars.EngineOverridePath); + get => _cfg.GetCVar(CVars.EngineOverridePath); set { - Cfg.SetCVar(CVars.EngineOverridePath, value); - Cfg.CommitConfig(); + _cfg.SetCVar(CVars.EngineOverridePath, value); + _cfg.CommitConfig(); } } } diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/HomePageViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/HomePageViewModel.cs index 363c6aac5..3c9f3fc05 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/HomePageViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/HomePageViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -7,7 +6,7 @@ using Avalonia.VisualTree; using DynamicData; using DynamicData.Alias; -using ReactiveUI.Fody.Helpers; +using Microsoft.Toolkit.Mvvm.ComponentModel; using Splat; using SS14.Launcher.Localization; using SS14.Launcher.Models.Data; @@ -17,7 +16,7 @@ namespace SS14.Launcher.ViewModels.MainWindowTabs; -public class HomePageViewModel : MainWindowTabViewModel +public partial class HomePageViewModel : MainWindowTabViewModel { public MainWindowViewModel MainWindowViewModel { get; } private readonly DataManager _cfg; @@ -58,9 +57,8 @@ public HomePageViewModel(MainWindowViewModel mainWindowViewModel) } public ReadOnlyObservableCollection Favorites { get; } - public ObservableCollection Suggestions { get; } = new(); - [Reactive] public bool FavoritesEmpty { get; private set; } = true; + [ObservableProperty] private bool _favoritesEmpty = true; public override string Name => LocalizationManager.Instance.GetString("tab-home-title"); public Control? Control { get; set; } diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/NewsTabViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/NewsTabViewModel.cs index 09410b657..f2e001099 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/NewsTabViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/NewsTabViewModel.cs @@ -1,32 +1,21 @@ using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Linq; using CodeHollow.FeedReader; -using ReactiveUI; +using Microsoft.Toolkit.Mvvm.ComponentModel; using SS14.Launcher.Localization; +using SS14.Launcher.Utility; namespace SS14.Launcher.ViewModels.MainWindowTabs; -public class NewsTabViewModel : MainWindowTabViewModel +public partial class NewsTabViewModel : MainWindowTabViewModel { - private bool _startedPullingNews; - private bool _newsPulled; - - public NewsTabViewModel() - { - NewsEntries = new ObservableCollection(new List()); - - this.WhenAnyValue(x => x.NewsPulled) - .Subscribe(_ => this.RaisePropertyChanged(nameof(NewsNotPulled))); - } + public ObservableList NewsEntries { get; } = []; + public override string Name => LocalizationManager.Instance.GetString("tab-news-title"); - public bool NewsPulled - { - get => _newsPulled; - set => this.RaiseAndSetIfChanged(ref _newsPulled, value); - } + private bool _startedPullingNews; - public bool NewsNotPulled => !NewsPulled; + [ObservableProperty] + private bool _newsPulled; public override void Selected() { @@ -38,22 +27,12 @@ public override void Selected() private async void PullNews() { if (_startedPullingNews) - { return; - } _startedPullingNews = true; var feed = await FeedReader.ReadAsync(ConfigConstants.NewsFeedUrl); - foreach (var feedItem in feed.Items) - { - NewsEntries.Add(new NewsEntryViewModel(feedItem.Title, new Uri(feedItem.Link))); - } - + NewsEntries.AddRange(feed.Items.Select(i => new NewsEntryViewModel(i.Title, new Uri(i.Link)))); NewsPulled = true; } - - public ObservableCollection NewsEntries { get; } - - public override string Name => LocalizationManager.Instance.GetString("tab-news-title"); } diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/ServerListFiltersViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/ServerListFiltersViewModel.cs index 939c5aeb8..e719c87c2 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/ServerListFiltersViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/ServerListFiltersViewModel.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Collections.Specialized; using System.Globalization; using System.Linq; using Microsoft.Toolkit.Mvvm.ComponentModel; @@ -22,17 +20,11 @@ public sealed partial class ServerListFiltersViewModel : ObservableObject private int _totalServers; private int _filteredServers; - private readonly FilterListCollection _filtersLanguage = new(); - private readonly FilterListCollection _filtersRegion = new(); - private readonly FilterListCollection _filtersRolePlay = new(); - private readonly FilterListCollection _filtersEighteenPlus = new(); - private readonly FilterListCollection _filtersHub = new(); - - public ObservableCollection FiltersLanguage => _filtersLanguage; - public ObservableCollection FiltersRegion => _filtersRegion; - public ObservableCollection FiltersRolePlay => _filtersRolePlay; - public ObservableCollection FiltersEighteenPlus => _filtersEighteenPlus; - public ObservableCollection FiltersHub => _filtersHub; + public ObservableList FiltersLanguage { get; } = []; + public ObservableList FiltersRegion { get; } = []; + public ObservableList FiltersRolePlay { get; } = []; + public ObservableList FiltersEighteenPlus { get; } = []; + public ObservableList FiltersHub { get; } = []; public ServerFilterViewModel FilterPlayerCountHideEmpty { get; } public ServerFilterViewModel FilterPlayerCountHideFull { get; } @@ -204,10 +196,10 @@ public void UpdatePresentFilters(IEnumerable servers) new ServerFilter(ServerFilterCategory.RolePlay, ServerFilter.DataUnspecified), this)); // Set. - _filtersLanguage.SetItems(filtersLanguage); - _filtersRegion.SetItems(filtersRegion); - _filtersRolePlay.SetItems(filtersRolePlay); - _filtersHub.SetItems(filtersHub); + FiltersLanguage.SetItems(filtersLanguage); + FiltersRegion.SetItems(filtersRegion); + FiltersRolePlay.SetItems(filtersRolePlay); + FiltersHub.SetItems(filtersHub); } public bool GetFilter(ServerFilterCategory category, string data) => GetFilter(new ServerFilter(category, data)); @@ -405,19 +397,4 @@ public override int Compare(ServerFilterViewModel x, ServerFilterViewModel y) return idxX.CompareTo(idxY); } } - - private sealed class FilterListCollection : ObservableCollection - { - public void SetItems(IEnumerable items) - { - Items.Clear(); - - foreach (var item in items) - { - Items.Add(item); - } - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - } } diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/ServerListTabViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/ServerListTabViewModel.cs index bd50c08a1..9c50396f7 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/ServerListTabViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/ServerListTabViewModel.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Collections.Specialized; -using System.Reactive.Linq; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; +using System.Linq; +using Avalonia.Threading; +using Microsoft.Toolkit.Mvvm.ComponentModel; using Splat; using SS14.Launcher.Localization; using SS14.Launcher.Models.ServerStatus; @@ -12,25 +11,36 @@ namespace SS14.Launcher.ViewModels.MainWindowTabs; -public class ServerListTabViewModel : MainWindowTabViewModel +public partial class ServerListTabViewModel : MainWindowTabViewModel { private readonly LocalizationManager _loc = LocalizationManager.Instance; private readonly MainWindowViewModel _windowVm; private readonly ServerListCache _serverListCache; - public ObservableCollection SearchedServers { get; } = new(); + public ObservableList SearchedServers { get; } = []; private string? _searchString; + private readonly DispatcherTimer _searchThrottle = new() { Interval = TimeSpan.FromMilliseconds(200) }; public override string Name => _loc.GetString("tab-servers-title"); public string? SearchString { get => _searchString; - set => this.RaiseAndSetIfChanged(ref _searchString, value); - } + set + { + if (_searchString == value) + return; + + OnPropertyChanging(); + _searchString = value; + OnPropertyChanged(); - private const int throttleMs = 200; + // Search string was changed, stop a potential old throttle timer and restart it + _searchThrottle.Stop(); + _searchThrottle.Start(); + } + } public bool SpinnerVisible => _serverListCache.Status < RefreshListStatus.Updated; @@ -62,7 +72,7 @@ public string ListText } } - [Reactive] public bool FiltersVisible { get; set; } + [ObservableProperty] private bool _filtersVisible; public ServerListFiltersViewModel Filters { get; } @@ -81,18 +91,20 @@ public ServerListTabViewModel(MainWindowViewModel windowVm) switch (args.PropertyName) { case nameof(ServerListCache.Status): - this.RaisePropertyChanged(nameof(ListText)); - this.RaisePropertyChanged(nameof(SpinnerVisible)); + OnPropertyChanged(nameof(ListText)); + OnPropertyChanged(nameof(SpinnerVisible)); break; } }; - _loc.LanguageSwitched += () => Filters.UpdatePresentFilters(_serverListCache.AllServers); + _searchThrottle.Tick += (_, _) => + { + // Interval since last search string change has passed, stop the timer and update the list + _searchThrottle.Stop(); + UpdateSearchedList(); + }; - this.WhenAnyValue(x => x.SearchString) - .Throttle(TimeSpan.FromMilliseconds(throttleMs), RxApp.MainThreadScheduler) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(_ => UpdateSearchedList()); + _loc.LanguageSwitched += () => Filters.UpdatePresentFilters(_serverListCache.AllServers); } private void FiltersOnFiltersUpdated() @@ -133,14 +145,10 @@ private void UpdateSearchedList() sortList.Sort(ServerSortComparer.Instance); - SearchedServers.Clear(); - foreach (var server in sortList) - { - var vm = new ServerEntryViewModel(_windowVm, server, _serverListCache, _windowVm.Cfg); - SearchedServers.Add(vm); - } + SearchedServers.SetItems(sortList.Select(server + => new ServerEntryViewModel(_windowVm, server, _serverListCache, _windowVm.Cfg))); - this.RaisePropertyChanged(nameof(ListText)); + OnPropertyChanged(nameof(ListText)); } private bool DoesSearchMatch(ServerStatusData data) diff --git a/SS14.Launcher/ViewModels/MainWindowViewModel.cs b/SS14.Launcher/ViewModels/MainWindowViewModel.cs index b24f048d4..8512c1915 100644 --- a/SS14.Launcher/ViewModels/MainWindowViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowViewModel.cs @@ -1,16 +1,15 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http; -using System.Reactive.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Avalonia.Platform.Storage; using DynamicData; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; +using Microsoft.Toolkit.Mvvm.ComponentModel; using Serilog; using Splat; using SS14.Launcher.Api; @@ -25,18 +24,17 @@ namespace SS14.Launcher.ViewModels; -public sealed class MainWindowViewModel : ViewModelBase, IErrorOverlayOwner +public sealed partial class MainWindowViewModel : ViewModelBase, IErrorOverlayOwner { private readonly DataManager _cfg; private readonly LoginManager _loginMgr; - private readonly HttpClient _http; private readonly LauncherInfoManager _infoManager; private readonly LocalizationManager _loc; private int _selectedIndex; public DataManager Cfg => _cfg; - [Reactive] public bool OutOfDate { get; private set; } + [ObservableProperty] private bool _outOfDate; public HomePageViewModel HomeTab { get; } public ServerListTabViewModel ServersTab { get; } @@ -47,7 +45,6 @@ public MainWindowViewModel() { _cfg = Locator.Current.GetRequiredService(); _loginMgr = Locator.Current.GetRequiredService(); - _http = Locator.Current.GetRequiredService(); _infoManager = Locator.Current.GetRequiredService(); _loc = LocalizationManager.Instance; @@ -56,45 +53,34 @@ public MainWindowViewModel() HomeTab = new HomePageViewModel(this); OptionsTab = new OptionsTabViewModel(); - var tabs = new List(); - tabs.Add(HomeTab); - tabs.Add(ServersTab); - tabs.Add(NewsTab); - tabs.Add(OptionsTab); + Tabs = new List + { + HomeTab, + ServersTab, + NewsTab, + OptionsTab, #if DEVELOPMENT - tabs.Add(new DevelopmentTabViewModel()); + new DevelopmentTabViewModel(), #endif - Tabs = tabs; + }; AccountDropDown = new AccountDropDownViewModel(this); LoginViewModel = new MainWindowLoginViewModel(); - this.WhenAnyValue(x => x._loginMgr.ActiveAccount) - .Subscribe(s => - { - this.RaisePropertyChanged(nameof(Username)); - this.RaisePropertyChanged(nameof(LoggedIn)); - }); + PropertyChanged += (_, e) => + { + if (e.PropertyName is nameof(LoggedIn) && LoggedIn) + RunSelectedOnTab(); + }; - _cfg.Logins.Connect() - .Subscribe(_ => { this.RaisePropertyChanged(nameof(AccountDropDownVisible)); }); + _loginMgr.PropertyChanged += (_, e) => + { + if (e.PropertyName is nameof(_loginMgr.ActiveAccount)) + OnPropertyChanged(new PropertyChangedEventArgs(nameof(LoggedIn))); + }; - // If we leave the login view model (by an account getting selected) - // we reset it to login state - this.WhenAnyValue(x => x.LoggedIn) - .DistinctUntilChanged() // Only when change. - .Subscribe(x => - { - if (x) - { - // "Switch" to main window. - RunSelectedOnTab(); - } - else - { - LoginViewModel.SwitchToLogin(); - } - }); + _cfg.Logins.Connect() + .Subscribe(_ => OnPropertyChanged(new PropertyChangedEventArgs(nameof(AccountDropDownVisible)))); } public MainWindow? Control { get; set; } @@ -102,17 +88,16 @@ public MainWindowViewModel() public IReadOnlyList Tabs { get; } public bool LoggedIn => _loginMgr.ActiveAccount != null; - private string? Username => _loginMgr.ActiveAccount?.Username; public bool AccountDropDownVisible => _loginMgr.Logins.Count != 0; public AccountDropDownViewModel AccountDropDown { get; } public MainWindowLoginViewModel LoginViewModel { get; } - [Reactive] public ConnectingViewModel? ConnectingVM { get; set; } + [ObservableProperty] private ConnectingViewModel? _connectingVM; - [Reactive] public string? BusyTask { get; private set; } - [Reactive] public ViewModelBase? OverlayViewModel { get; private set; } + [ObservableProperty] private string? _busyTask; + [ObservableProperty] private ViewModelBase? _overlayViewModel; public int SelectedIndex { @@ -122,7 +107,12 @@ public int SelectedIndex var previous = Tabs[_selectedIndex]; previous.IsSelected = false; - this.RaiseAndSetIfChanged(ref _selectedIndex, value); + if (!EqualityComparer.Default.Equals(_selectedIndex, value)) + { + OnPropertyChanging(); + _selectedIndex = value; + OnPropertyChanged(); + } RunSelectedOnTab(); } @@ -214,14 +204,14 @@ public void DismissIntelDegradationPressed() { Cfg.SetCVar(CVars.HasDismissedIntelDegradation, true); Cfg.CommitConfig(); - this.RaisePropertyChanged(nameof(ShouldShowIntelDegradationWarning)); + OnPropertyChanged(nameof(ShouldShowIntelDegradationWarning)); } public void DismissAppleSiliconRosettaPressed() { Cfg.SetCVar(CVars.HasDismissedRosettaWarning, true); Cfg.CommitConfig(); - this.RaisePropertyChanged(nameof(ShouldShowRosettaWarning)); + OnPropertyChanged(nameof(ShouldShowRosettaWarning)); } public void SelectTabServers() diff --git a/SS14.Launcher/ViewModels/ViewModelBase.cs b/SS14.Launcher/ViewModels/ViewModelBase.cs index 5f4d25c77..35049669d 100644 --- a/SS14.Launcher/ViewModels/ViewModelBase.cs +++ b/SS14.Launcher/ViewModels/ViewModelBase.cs @@ -1,14 +1,10 @@ -using ReactiveUI; +using Microsoft.Toolkit.Mvvm.ComponentModel; namespace SS14.Launcher.ViewModels; -public class ViewModelBase : ReactiveObject, IViewModelBase -{ -} +public class ViewModelBase : ObservableObject, IViewModelBase; /// /// Signifies to that this viewmodel can be automatically located. /// -public interface IViewModelBase -{ -} +public interface IViewModelBase; diff --git a/SS14.Launcher/Views/AddFavoriteDialog.xaml b/SS14.Launcher/Views/AddFavoriteDialog.xaml index dfe7cf447..a9d812ba4 100644 --- a/SS14.Launcher/Views/AddFavoriteDialog.xaml +++ b/SS14.Launcher/Views/AddFavoriteDialog.xaml @@ -14,14 +14,16 @@ -