📦 🐛 We now use normal routing and LiteDb for storing preferences and such, fixes crash, Updates to AvaloniaUI 11.2.0

This commit is contained in:
GalaxyLittlepaws
2024-10-31 06:14:15 -04:00
parent a3c4266f26
commit 331bea3d9f
17 changed files with 576 additions and 464 deletions
@@ -13,9 +13,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Desktop" Version="11.1.4" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.0" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.4" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.0" />
</ItemGroup>
<ItemGroup>
+1
View File
@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeEditing/SuppressNullableWarningFix/Enabled/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/Naming/CSharpNaming/ApplyAutoDetectedRules/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=hyperlegible/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=metaverse/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
+86 -61
View File
@@ -1,116 +1,141 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
using GalaxyViewer.Models;
using GalaxyViewer.Services;
using GalaxyViewer.ViewModels;
using GalaxyViewer.Views;
using GalaxyViewer.Services;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
namespace GalaxyViewer;
public class App : Application
public class App : Application, IDisposable
{
private static IServiceProvider? _serviceProvider;
public static PreferencesManager? PreferencesManager { get; private set; }
public App()
{
// Initialize Serilog here
ConfigureLogging();
}
private static void ConfigureLogging()
{
var logFilePath =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"GalaxyViewer", "logs", "error.log");
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("logs/error.log", rollingInterval: RollingInterval.Day)
.WriteTo.File(logFilePath, rollingInterval: RollingInterval.Day)
.CreateLogger();
}
public static PreferencesManager? PreferencesManager { get; private set; }
public static IServiceProvider? ServiceProvider { get; private set; }
public override void Initialize()
{
PreferencesManager = new PreferencesManager(new PreferencesModel());
PreferencesManager.PreferencesChanged += OnPreferencesChanged;
var serviceCollection = new ServiceCollection();
ConfigureServices(serviceCollection);
ServiceProvider = serviceCollection.BuildServiceProvider();
AvaloniaXamlLoader.Load(this);
base.Initialize();
}
private void ConfigureServices(IServiceCollection services)
private static void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<LiteDbService>();
// Register other services here
}
public override async void OnFrameworkInitializationCompleted()
public override void Initialize()
{
var contentControl = new ContentControl();
var navigationService = new NavigationService(contentControl);
var serviceCollection = new ServiceCollection();
ConfigureServices(serviceCollection);
_serviceProvider = serviceCollection.BuildServiceProvider();
// Register routes
navigationService.RegisterRoute("login", typeof(LoginView));
navigationService.RegisterRoute("main", typeof(LoggedInView));
navigationService.RegisterRoute("debug", typeof(DebugView));
navigationService.RegisterRoute("preferences", typeof(PreferencesView));
switch (ApplicationLifetime)
var liteDbService = _serviceProvider.GetService<LiteDbService>();
if (liteDbService == null)
{
case IClassicDesktopStyleApplicationLifetime desktop:
desktop.MainWindow = new MainWindow
{
DataContext = new MainViewModel(navigationService)
};
break;
case ISingleViewApplicationLifetime singleViewPlatform:
singleViewPlatform.MainView = new MainView
{
DataContext = new MainViewModel(navigationService)
};
break;
throw new InvalidOperationException("LiteDbService is not registered.");
}
Debug.Assert(PreferencesManager != null, nameof(PreferencesManager) + " != null");
var preferences = await PreferencesManager.LoadPreferencesAsync();
ApplyPreferences(preferences);
PreferencesManager = new PreferencesManager(liteDbService);
PreferencesManager.PreferencesChanged += OnPreferencesChanged;
AvaloniaXamlLoader.Load(this);
base.Initialize();
}
public override void OnFrameworkInitializationCompleted()
{
try
{
switch (ApplicationLifetime)
{
case IClassicDesktopStyleApplicationLifetime desktop:
Log.Information("Initializing MainWindow for desktop application.");
desktop.MainWindow = new MainWindow
{
DataContext = new MainViewModel()
};
desktop.MainWindow.Show();
break;
case ISingleViewApplicationLifetime singleViewPlatform:
Log.Information("Initializing MainView for single view application.");
singleViewPlatform.MainView = new MainView
{
DataContext = new MainViewModel()
};
break;
}
}
catch (Exception ex)
{
Log.Error(ex, "An error occurred while initializing the main window.");
throw; // Optionally rethrow the exception if you want to halt the application
}
base.OnFrameworkInitializationCompleted();
}
private void OnPreferencesChanged(object? sender, PreferencesModel preferences)
{
if (PreferencesManager?.IsLoadingPreferences == true) return;
ApplyPreferences(preferences);
RefreshThemeForAllWindows();
}
private void ApplyPreferences(PreferencesModel preferences)
{
RequestedThemeVariant = preferences.Theme switch
Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
{
"Light" => ThemeVariant.Light,
"Dark" => ThemeVariant.Dark,
_ => ThemeVariant.Default
};
RequestedThemeVariant = preferences.Theme switch
{
"Light" => ThemeVariant.Light,
"Dark" => ThemeVariant.Dark,
_ => ThemeVariant.Default
};
// TODO: Apply other preferences
// TODO: Apply other preferences
});
}
private void RefreshThemeForAllWindows()
private async void RefreshThemeForAllWindows()
{
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktopLifetime)
return;
foreach (var window in desktopLifetime.Windows)
await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(async () =>
{
if (window is not BaseWindow baseWindow) continue;
var resultTheme = PreferencesManager?.LoadPreferencesAsync().Result.Theme;
if (resultTheme != null)
baseWindow.ApplyTheme(resultTheme);
}
foreach (var window in desktopLifetime.Windows)
{
if (window is not BaseWindow baseWindow) continue;
var resultTheme = (await PreferencesManager?.LoadPreferencesAsync())?.Theme;
if (resultTheme != null)
baseWindow.ApplyTheme(resultTheme);
}
});
}
public void Dispose()
{
//PreferencesManager?.Dispose();
(_serviceProvider as IDisposable)?.Dispose();
}
}
+5 -5
View File
@@ -27,12 +27,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.1.4" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.4" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.4" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.1.4" />
<PackageReference Include="Avalonia" Version="11.2.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.0" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.0" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.0" />
<!-- Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration. -->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.4" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.0" />
<!-- More things will go here -->
<PackageReference Include="AvaloniaInside.Shell" Version="1.2.0" />
<PackageReference Include="Irihi.Ursa" Version="1.4.0" />
+10
View File
@@ -0,0 +1,10 @@
using ReactiveUI;
namespace GalaxyViewer.Models
{
public class DebugViewModel(IScreen screen) : ReactiveObject, IRoutableViewModel
{
public string UrlPathSegment => "debug";
public IScreen HostScreen { get; } = screen;
}
}
+11 -19
View File
@@ -1,22 +1,14 @@
using System;
using System.Collections.Generic;
using LiteDB;
namespace GalaxyViewer.Models;
[Serializable]
public class PreferencesModel
namespace GalaxyViewer.Models
{
public string Theme { get; set; } = "Default";
public string LoginLocation { get; set; } = "Home";
public string Font { get; set; } = "Atkinson Hyperlegible";
public string Language { get; set; } = "en-US";
public long LastSavedEpoch { get; set; }
}
public static class PreferencesOptions
{
public static readonly List<string> ThemeOptions = ["Light", "Dark", "Default"];
public static readonly List<string> LoginLocationOptions = ["Home", "Last Location"];
public static readonly List<string> FontOptions = ["Inter", "Atkinson Hyperlegible"];
public static readonly List<string> LanguageOptions = ["en-US"];
public class PreferencesModel
{
[BsonId] public ObjectId Id { get; set; }
public string Theme { get; set; }
public string LoginLocation { get; set; }
public string Font { get; set; }
public string Language { get; set; }
public long LastSavedEpoch { get; set; }
}
}
+11
View File
@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace GalaxyViewer.Models;
public static class PreferencesOptions
{
public static readonly List<string> ThemeOptions = ["Light", "Dark", "Default"];
public static readonly List<string> LoginLocationOptions = ["Home", "Last Location"];
public static readonly List<string> FontOptions = ["Inter", "Atkinson Hyperlegible"];
public static readonly List<string> LanguageOptions = ["en-US"];
}
+74 -16
View File
@@ -1,26 +1,84 @@
using LiteDB;
using System;
using System.IO;
using System.Threading.Tasks;
using GalaxyViewer.Models;
using Serilog;
namespace GalaxyViewer.Services;
public class LiteDbService : IDisposable
namespace GalaxyViewer.Services
{
private readonly LiteDatabase _database;
public LiteDbService()
public class LiteDbService : IDisposable
{
var dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GalaxyViewer", "data.db");
_database = new LiteDatabase(dbPath);
}
private readonly LiteDatabase? _database;
public ILiteCollection<T> GetCollection<T>(string name)
{
return _database.GetCollection<T>(name);
}
public LiteDbService()
{
try
{
var dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GalaxyViewer", "data.db");
Directory.CreateDirectory(Path.GetDirectoryName(dbPath) ?? throw new InvalidOperationException()); // Ensure the directory exists
_database = new LiteDatabase(dbPath);
Log.Information("LiteDbService initialized with database path: {DbPath}", dbPath);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to initialize LiteDbService");
throw;
}
}
public void Dispose()
{
_database?.Dispose();
public ILiteCollection<T> GetCollection<T>(string name)
{
Log.Information("Retrieving collection: {CollectionName}", name);
return _database.GetCollection<T>(name);
}
public LiteDatabase? Database()
{
return _database;
}
public void SeedDatabase()
{
try
{
var preferencesCollection = _database.GetCollection<PreferencesModel>("preferences");
var count = preferencesCollection.Count();
Log.Information("Number of records in preferences collection: {Count}", count);
if (count == 0)
{
var defaultPreferences = new PreferencesModel
{
Theme = "Default",
LoginLocation = "Home",
Font = "Atkinson Hyperlegible",
Language = "en-US",
LastSavedEpoch = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
preferencesCollection.Insert(defaultPreferences);
Log.Information("Database seeded with default preferences");
}
count = preferencesCollection.Count();
Log.Information("Number of records in preferences collection: {Count}", count);
var allPreferences = preferencesCollection.FindAll();
foreach (var preference in allPreferences)
{
Log.Information("Preference: {@Preference}", preference);
}
}
catch (Exception ex)
{
Log.Error(ex, "Failed to seed database");
}
}
public void Dispose()
{
_database?.Dispose();
Log.Information("LiteDbService disposed");
}
}
}
+32 -53
View File
@@ -1,75 +1,54 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Xml.Serialization;
using System.Diagnostics;
using LiteDB;
using GalaxyViewer.Models;
using Serilog;
using System.Threading.Tasks;
namespace GalaxyViewer.Services
{
public class PreferencesManager
public sealed class PreferencesManager
{
private readonly string _preferencesFilePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GalaxyViewer",
"preferences.xml");
private readonly ILiteCollection<PreferencesModel> _preferencesCollection;
public PreferencesModel CurrentPreferences { get; private set; }
public event EventHandler<PreferencesModel>? PreferencesChanged;
public PreferencesManager(PreferencesModel currentPreferences)
public PreferencesManager(LiteDbService? liteDbService)
{
CurrentPreferences = currentPreferences;
EnsurePreferencesDirectory();
}
private void EnsurePreferencesDirectory()
{
var directoryPath = Path.GetDirectoryName(_preferencesFilePath);
if (!string.IsNullOrEmpty(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
Debug.Assert(liteDbService != null, nameof(liteDbService) + " != null");
var database = liteDbService.Database();
Debug.Assert(database != null, nameof(database) + " != null");
_preferencesCollection = database.GetCollection<PreferencesModel>("preferences");
}
public async Task<PreferencesModel> LoadPreferencesAsync()
{
try
return await Task.Run(() =>
{
await using var stream = new FileStream(_preferencesFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
var serializer = new XmlSerializer(typeof(PreferencesModel));
CurrentPreferences = (PreferencesModel)serializer.Deserialize(stream)!;
IsLoadingPreferences = false;
return CurrentPreferences;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to load preferences");
CurrentPreferences = new PreferencesModel();
IsLoadingPreferences = false;
return CurrentPreferences;
}
var preferences = _preferencesCollection.FindOne(Query.All());
return preferences ?? new PreferencesModel
{
Id = ObjectId.NewObjectId(),
Theme = "Default",
LoginLocation = "Home",
Font = "Atkinson Hyperlegible",
Language = "en-US",
LastSavedEpoch = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
});
}
public bool IsLoadingPreferences { get; private set; }
public event EventHandler<PreferencesModel>? PreferencesChanged;
public async Task SavePreferencesAsync(PreferencesModel preferences)
{
try
await Task.Run(() =>
{
await using var stream = new FileStream(_preferencesFilePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
var serializer = new XmlSerializer(typeof(PreferencesModel));
serializer.Serialize(stream, preferences);
CurrentPreferences = preferences;
if (!IsLoadingPreferences)
{
PreferencesChanged?.Invoke(this, preferences);
}
}
catch (Exception ex)
{
Log.Error(ex, "Failed to save preferences");
}
_preferencesCollection.Upsert(preferences);
OnPreferencesChanged(preferences);
});
}
private void OnPreferencesChanged(PreferencesModel preferences)
{
PreferencesChanged?.Invoke(this, preferences);
}
}
}
+148 -137
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Reactive;
using System.Runtime.InteropServices;
@@ -11,158 +12,168 @@ using Ursa.Controls;
using Serilog;
using OpenMetaverse;
namespace GalaxyViewer.ViewModels;
public class LoginViewModel : ReactiveObject
namespace GalaxyViewer.ViewModels
{
private readonly PreferencesViewModel _preferencesViewModel;
private string _username;
private string _password;
private readonly GridClient _client = new();
public WindowToastManager? ToastManager { get; set; }
public LoginViewModel(PreferencesViewModel preferencesViewModel, string username,
string password)
public class LoginViewModel : ReactiveObject, IRoutableViewModel
{
_preferencesViewModel = preferencesViewModel;
_username = username;
_password = password;
LoginLocations = _preferencesViewModel.LoginLocationOptions;
SelectedLoginLocation = _preferencesViewModel.SelectedLoginLocation;
TryLoginCommand = ReactiveCommand.CreateFromTask(TryLoginAsync);
// Subscribe to the ThrownExceptions property to handle errors
TryLoginCommand.ThrownExceptions.Subscribe(ex =>
public string UrlPathSegment => "login";
public IScreen HostScreen
{
Log.Error("An error occurred during login: {Error}", ex.Message);
// Handle the error (e.g., show a dialog to the user)
_ = ShowLoginErrorAsync("An error occurred during login. Please try again.");
});
}
public ObservableCollection<string> LoginLocations { get; }
public string Username
{
get => _username;
set => this.RaiseAndSetIfChanged(ref _username, value);
}
public string Password
{
get => _password;
set => this.RaiseAndSetIfChanged(ref _password, value);
}
public string SelectedLoginLocation
{
get => _preferencesViewModel.SelectedLoginLocation;
set
{
_preferencesViewModel.SelectedLoginLocation = value;
this.RaisePropertyChanged();
}
}
public ReactiveCommand<Unit, Unit> TryLoginCommand { get; }
private async Task ShowLoginErrorAsync(string errorMessage)
{
ToastManager?.Show(
new Toast(
errorMessage),
showIcon: true,
showClose: true,
type: NotificationType.Error
);
await Task.CompletedTask;
}
private async Task TryLoginAsync()
{
if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password))
{
Log.Warning("Username or password is empty");
await ShowLoginErrorAsync("Username or password is empty");
return;
}
var platformMap = new Dictionary<OSPlatform, string>
{
{ OSPlatform.Windows, "Win" },
{ OSPlatform.Linux, "Lin" },
{ OSPlatform.OSX, "Mac" },
{ OSPlatform.Create("ANDROID"), "And" },
{ OSPlatform.Create("IOS"), "iOS" }
};
var ourPlatform =
platformMap.FirstOrDefault(kv => RuntimeInformation.IsOSPlatform(kv.Key)).Value ??
"Unk";
var loginParams = _client.Network.DefaultLoginParams(
Username.Split(' ')[0], // firstName
Username.Contains(' ') ? Username.Split(' ')[1] : "Resident", // lastName
Password,
"GalaxyViewer", // ViewerName
"0.1.0" // ViewerVersion
);
loginParams.URI =
"https://login.agni.lindenlab.com/cgi-bin/login.cgi"; // Set the login URI to SL main grid for now TODO: Update to use the selected grid in login menu
loginParams.MfaEnabled = true; // Inform the server that we support MFA
loginParams.Platform = ourPlatform; // Set the platform - our operating system
loginParams.PlatformVersion =
Environment.OSVersion.VersionString; // Set the platform version
loginParams.Start =
_preferencesViewModel
.SelectedLoginLocation; // Set the start location to the selected login location
loginParams.MfaHash = string.Empty; // Clear the MFA hash
var loginSuccess = await Task.Run(() => _client.Network.Login(loginParams));
if (loginSuccess)
{
Log.Information("Login successful");
}
else if (_client.Network.LoginMessage.Contains("MFA required"))
{
Log.Warning("MFA required");
var mfaCode = await ShowMfaInputDialogAsync();
if (!string.IsNullOrEmpty(mfaCode))
get
{
loginParams.MfaHash = Utils.MD5(mfaCode);
loginSuccess = await Task.Run(() => _client.Network.Login(loginParams));
if (loginSuccess)
Debug.Assert(_routableViewModelImplementation?.HostScreen != null, "_routableViewModelImplementation?.HostScreen != null");
return _routableViewModelImplementation.HostScreen;
}
}
private readonly PreferencesViewModel _preferencesViewModel;
private string _username;
private string _password;
private readonly GridClient _client = new();
private IRoutableViewModel? _routableViewModelImplementation;
public WindowToastManager? ToastManager { get; set; }
public LoginViewModel()
{
_preferencesViewModel = new PreferencesViewModel(); // Initialize as needed
_username = string.Empty;
_password = string.Empty;
LoginLocations = _preferencesViewModel.LoginLocationOptions;
SelectedLoginLocation = _preferencesViewModel.SelectedLoginLocation;
TryLoginCommand = ReactiveCommand.CreateFromTask(TryLoginAsync);
// Subscribe to the ThrownExceptions property to handle errors
TryLoginCommand.ThrownExceptions.Subscribe(ex =>
{
Log.Error("An error occurred during login: {Error}", ex.Message);
// Handle the error (e.g., show a dialog to the user)
_ = ShowLoginErrorAsync("An error occurred during login. Please try again.");
});
}
public ObservableCollection<string> LoginLocations { get; }
public string Username
{
get => _username;
set => this.RaiseAndSetIfChanged(ref _username, value);
}
public string Password
{
get => _password;
set => this.RaiseAndSetIfChanged(ref _password, value);
}
public string SelectedLoginLocation
{
get => _preferencesViewModel.SelectedLoginLocation;
set
{
_preferencesViewModel.SelectedLoginLocation = value;
this.RaisePropertyChanged();
}
}
public ReactiveCommand<Unit, Unit> TryLoginCommand { get; }
private async Task ShowLoginErrorAsync(string errorMessage)
{
ToastManager?.Show(
new Toast(
errorMessage),
showIcon: true,
showClose: true,
type: NotificationType.Error
);
await Task.CompletedTask;
}
private async Task TryLoginAsync()
{
if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password))
{
Log.Warning("Username or password is empty");
await ShowLoginErrorAsync("Username or password is empty");
return;
}
var platformMap = new Dictionary<OSPlatform, string>
{
{ OSPlatform.Windows, "Win" },
{ OSPlatform.Linux, "Lin" },
{ OSPlatform.OSX, "Mac" },
{ OSPlatform.Create("ANDROID"), "And" },
{ OSPlatform.Create("IOS"), "iOS" }
};
var ourPlatform =
platformMap.FirstOrDefault(kv => RuntimeInformation.IsOSPlatform(kv.Key)).Value ??
"Unk";
var loginParams = _client.Network.DefaultLoginParams(
Username.Split(' ')[0], // firstName
Username.Contains(' ') ? Username.Split(' ')[1] : "Resident", // lastName
Password,
"GalaxyViewer", // ViewerName
"0.1.0" // ViewerVersion
);
loginParams.URI =
"https://login.agni.lindenlab.com/cgi-bin/login.cgi"; // Set the login URI to SL main grid for now TODO: Update to use the selected grid in login menu
loginParams.MfaEnabled = true; // Inform the server that we support MFA
loginParams.Platform = ourPlatform; // Set the platform - our operating system
loginParams.PlatformVersion =
Environment.OSVersion.VersionString; // Set the platform version
loginParams.Start =
_preferencesViewModel
.SelectedLoginLocation; // Set the start location to the selected login location
loginParams.MfaHash = string.Empty; // Clear the MFA hash
var loginSuccess = await Task.Run(() => _client.Network.Login(loginParams));
if (loginSuccess)
{
Log.Information("Login successful");
}
else if (_client.Network.LoginMessage.Contains("MFA required"))
{
Log.Warning("MFA required");
var mfaCode = await ShowMfaInputDialogAsync();
if (!string.IsNullOrEmpty(mfaCode))
{
Log.Information("Login successful with MFA");
loginParams.MfaHash = Utils.MD5(mfaCode);
loginSuccess = await Task.Run(() => _client.Network.Login(loginParams));
if (loginSuccess)
{
Log.Information("Login successful with MFA");
}
else
{
Log.Error("Login failed with MFA: {Error}", _client.Network.LoginMessage);
await ShowLoginErrorAsync(
$"Login failed with MFA: {_client.Network.LoginMessage}");
}
}
else
{
Log.Error("Login failed with MFA: {Error}", _client.Network.LoginMessage);
await ShowLoginErrorAsync(
$"Login failed with MFA: {_client.Network.LoginMessage}");
Log.Warning("MFA code entry was canceled");
await ShowLoginErrorAsync("MFA code entry was canceled");
}
}
else
{
Log.Warning("MFA code entry was canceled");
await ShowLoginErrorAsync("MFA code entry was canceled");
Log.Error("Login failed: {Error}", _client.Network.LoginMessage);
await ShowLoginErrorAsync($"Login failed: {_client.Network.LoginMessage}");
}
}
else
private async Task<string> ShowMfaInputDialogAsync()
{
Log.Error("Login failed: {Error}", _client.Network.LoginMessage);
await ShowLoginErrorAsync($"Login failed: {_client.Network.LoginMessage}");
// TODO: Implement MFA input dialog
return await Task.FromResult(string.Empty);
}
}
private async Task<string> ShowMfaInputDialogAsync()
{
// TODO: Implement MFA input dialog
return await Task.FromResult(string.Empty);
}
}
+66 -37
View File
@@ -1,25 +1,26 @@
using System.Reactive;
using System.Runtime.InteropServices;
using System;
using System.IO;
using System.Reactive;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using GalaxyViewer.Services;
using GalaxyViewer.Views;
using OpenMetaverse;
using ReactiveUI;
using Serilog;
namespace GalaxyViewer.ViewModels
{
public class MainViewModel : ViewModelBase
{
public bool IsMobile { get; }
private UserControl? _currentView;
private bool _isLoggedIn;
private readonly GridClient _client = new();
private string? _username;
public string? Username
{
get => _username;
@@ -27,7 +28,6 @@ namespace GalaxyViewer.ViewModels
}
private string? _password;
public string? Password
{
get => _password;
@@ -35,7 +35,6 @@ namespace GalaxyViewer.ViewModels
}
private string? _loginLocation;
public string? LoginLocation
{
get => _loginLocation;
@@ -43,50 +42,42 @@ namespace GalaxyViewer.ViewModels
}
private string? _grid;
public string? Grid
{
get => _grid;
set => this.RaiseAndSetIfChanged(ref _grid, value);
}
public UserControl? CurrentView
{
get => _currentView;
set => this.RaiseAndSetIfChanged(ref _currentView, value);
}
public bool IsLoggedIn
{
get => _isLoggedIn;
set => this.RaiseAndSetIfChanged(ref _isLoggedIn, value);
set
{
this.RaiseAndSetIfChanged(ref _isLoggedIn, value);
CurrentView = value ? new LoggedInView() : new LoginView();
}
}
public ReactiveCommand<Unit, Unit> LogoutCommand { get; }
public ReactiveCommand<Unit, Unit> LoginCommand { get; }
public ICommand ShowChatCommand { get; }
public ICommand ShowPreferencesCommand { get; }
public ICommand ShowDevViewCommand { get; }
public ICommand ExitCommand { get; }
public MainViewModel(NavigationService navigationService)
public MainViewModel()
{
IsMobile = RuntimeInformation.IsOSPlatform(OSPlatform.Create("ANDROID"));
_currentView = new LoginView();
IsLoggedIn = false; // By default you aren't logged in
LogoutCommand = ReactiveCommand.Create(Logout);
LoginCommand = ReactiveCommand.Create(() => navigationService.NavigateTo("login"));
ShowChatCommand = ReactiveCommand.Create(() =>
navigationService.NavigateTo("chat"));
LoginCommand = ReactiveCommand.CreateFromTask(Login);
ShowPreferencesCommand = ReactiveCommand.Create(ShowPreferences);
ShowDevViewCommand = ReactiveCommand.Create(() => navigationService.NavigateTo("debug"));
ExitCommand = ReactiveCommand.Create(ExitApplication);
// NavigateTo to login view on startup
if (!IsLoggedIn)
{
navigationService.NavigateTo("login");
}
}
private void Logout()
@@ -96,10 +87,44 @@ namespace GalaxyViewer.ViewModels
IsLoggedIn = false;
}
private static void ShowPreferences()
private async Task Login()
{
try
{
// Validate properties before using them
if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password))
{
// Handle invalid login parameters
await File.AppendAllTextAsync("error.log", $"Invalid login parameters for user: {Username}");
return; // Exit the method if validation fails
}
const string userAgent = "GalaxyViewer/0.1.0";
var loginParams = _client.Network.DefaultLoginParams(Username, Password, userAgent, LoginLocation, Grid);
var loginSuccess = await Task.Run(() => _client.Network.Login(loginParams));
if (loginSuccess)
{
IsLoggedIn = true;
}
else
{
// Handle failed login
Log.Error("Failed to login user: {Username}", Username);
}
}
catch (Exception ex)
{
// Handle any exceptions that occur during login
Log.Error(ex, "An error occurred while logging in user: {Username}", Username);
}
}
private void ShowPreferences()
{
#if ANDROID
_navigationService.NavigateTo("preferences");
CurrentView = new PreferencesView();
#else
var preferencesWindow = new PreferencesWindow
{
@@ -111,11 +136,15 @@ namespace GalaxyViewer.ViewModels
private void ExitApplication()
{
if (Application.Current?.ApplicationLifetime is not
IClassicDesktopStyleApplicationLifetime
desktopLifetime) return;
Logout();
desktopLifetime.Shutdown();
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
desktopLifetime.Shutdown();
}
}
public void Dispose()
{
_client.Network.Logout();
}
}
}
+38 -50
View File
@@ -1,3 +1,4 @@
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows.Input;
@@ -15,16 +16,15 @@ namespace GalaxyViewer.ViewModels
public PreferencesViewModel()
{
if (App.PreferencesManager != null) _preferencesManager = App.PreferencesManager;
_preferences = App.PreferencesManager?.CurrentPreferences ?? new PreferencesModel();
ThemeOptions = new ObservableCollection<string>(_preferences.ThemeOptions);
LoginLocationOptions = new ObservableCollection<string>(_preferences.LoginLocationOptions);
LanguageOptions = new ObservableCollection<string>(_preferences.LanguageOptions);
FontOptions = new ObservableCollection<string>(_preferences.FontOptions);
_selectedTheme = _preferences.Theme;
_selectedLoginLocation = _preferences.LoginLocation;
_selectedLanguage = _preferences.Language;
_selectedFont = _preferences.Font;
_preferencesManager = App.PreferencesManager ??
throw new InvalidOperationException(
"PreferencesManager is not initialized.");
LoadPreferences();
ThemeOptions = new ObservableCollection<string>(PreferencesOptions.ThemeOptions);
LoginLocationOptions =
new ObservableCollection<string>(PreferencesOptions.LoginLocationOptions);
LanguageOptions = new ObservableCollection<string>(PreferencesOptions.LanguageOptions);
FontOptions = new ObservableCollection<string>(PreferencesOptions.FontOptions);
SaveCommand = new RelayCommand(async () => await SavePreferencesAsync());
}
@@ -34,15 +34,11 @@ namespace GalaxyViewer.ViewModels
if (_preferencesManager != null)
_preferences = await _preferencesManager.LoadPreferencesAsync();
// Set selected options from preferences.xml
if (_selectedTheme != _preferences.Theme)
_selectedTheme = _preferences.Theme;
if (_selectedLoginLocation != _preferences.LoginLocation)
_selectedLoginLocation = _preferences.LoginLocation;
if (_selectedLanguage != _preferences.Language)
_selectedLanguage = _preferences.Language;
if (_selectedFont != _preferences.Font)
_selectedFont = _preferences.Font;
// Set selected options from preferences
_selectedTheme = _preferences.Theme;
_selectedLoginLocation = _preferences.LoginLocation;
_selectedLanguage = _preferences.Language;
_selectedFont = _preferences.Font;
OnPropertyChanged(nameof(SelectedTheme));
OnPropertyChanged(nameof(SelectedLoginLocation));
@@ -51,6 +47,17 @@ namespace GalaxyViewer.ViewModels
_isLoadingPreferences = false;
}
private async Task SavePreferencesAsync()
{
_preferences.Theme = SelectedTheme;
_preferences.LoginLocation = SelectedLoginLocation;
_preferences.Language = SelectedLanguage;
_preferences.Font = SelectedFont;
if (_preferencesManager != null)
await _preferencesManager.SavePreferencesAsync(_preferences);
}
public ObservableCollection<string> ThemeOptions { get; private set; }
public ObservableCollection<string> LoginLocationOptions { get; private set; }
public ObservableCollection<string> LanguageOptions { get; private set; }
@@ -66,11 +73,9 @@ namespace GalaxyViewer.ViewModels
get => _selectedTheme;
set
{
if (_selectedTheme != value && !_isLoadingPreferences)
{
_selectedTheme = value;
OnPropertyChanged(nameof(SelectedTheme));
}
if (_selectedTheme == value || _isLoadingPreferences) return;
_selectedTheme = value;
OnPropertyChanged(nameof(SelectedTheme));
}
}
@@ -79,11 +84,9 @@ namespace GalaxyViewer.ViewModels
get => _selectedLoginLocation;
set
{
if (_selectedLoginLocation != value && !_isLoadingPreferences)
{
_selectedLoginLocation = value;
OnPropertyChanged(nameof(SelectedLoginLocation));
}
if (_selectedLoginLocation == value || _isLoadingPreferences) return;
_selectedLoginLocation = value;
OnPropertyChanged(nameof(SelectedLoginLocation));
}
}
@@ -92,11 +95,9 @@ namespace GalaxyViewer.ViewModels
get => _selectedLanguage;
set
{
if (_selectedLanguage != value && !_isLoadingPreferences)
{
_selectedLanguage = value;
OnPropertyChanged(nameof(SelectedLanguage));
}
if (_selectedLanguage == value || _isLoadingPreferences) return;
_selectedLanguage = value;
OnPropertyChanged(nameof(SelectedLanguage));
}
}
@@ -105,25 +106,12 @@ namespace GalaxyViewer.ViewModels
get => _selectedFont;
set
{
if (_selectedFont != value && !_isLoadingPreferences)
{
_selectedFont = value;
OnPropertyChanged(nameof(SelectedFont));
}
if (_selectedFont == value || _isLoadingPreferences) return;
_selectedFont = value;
OnPropertyChanged(nameof(SelectedFont));
}
}
private async Task SavePreferencesAsync()
{
_preferences.Theme = SelectedTheme;
_preferences.LoginLocation = SelectedLoginLocation;
_preferences.Language = SelectedLanguage;
_preferences.Font = SelectedFont;
if (_preferencesManager != null)
await _preferencesManager.SavePreferencesAsync(_preferences);
}
public ICommand SaveCommand { get; }
}
}
+33 -13
View File
@@ -1,4 +1,6 @@
using Avalonia.Controls;
using System.Diagnostics;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Styling;
using GalaxyViewer.Models;
@@ -11,25 +13,43 @@ public class BaseWindow : Window
{
Icon = new WindowIcon("Assets/GalaxyViewerLogo.ico");
CanResize = true;
App.PreferencesManager!.PreferencesChanged += OnPreferencesChanged;
var preferences = App.PreferencesManager.LoadPreferencesAsync().Result;
ApplyTheme(preferences.Theme);
FontFamily = new FontFamily(preferences.Font);
if (App.PreferencesManager != null)
App.PreferencesManager.PreferencesChanged += OnPreferencesChanged;
// Load preferences asynchronously without blocking the UI thread
_ = LoadPreferencesAsync();
}
private async Task LoadPreferencesAsync()
{
if (App.PreferencesManager == null) return;
var preferences = await App.PreferencesManager.LoadPreferencesAsync();
await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
{
ApplyTheme(preferences.Theme);
FontFamily = new FontFamily(preferences.Font);
});
}
private void OnPreferencesChanged(object? sender, PreferencesModel preferences)
{
ApplyTheme(preferences.Theme);
FontFamily = new FontFamily(preferences.Font);
Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
{
ApplyTheme(preferences.Theme);
FontFamily = new FontFamily(preferences.Font);
});
}
public void ApplyTheme(string theme)
internal void ApplyTheme(string theme)
{
RequestedThemeVariant = theme switch
Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
{
"Light" => ThemeVariant.Light,
"Dark" => ThemeVariant.Dark,
_ => ThemeVariant.Default
};
RequestedThemeVariant = theme switch
{
"Light" => ThemeVariant.Light,
"Dark" => ThemeVariant.Dark,
_ => ThemeVariant.Default
};
});
}
}
+1 -7
View File
@@ -1,6 +1,5 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using GalaxyViewer.Assets.Localization;
using GalaxyViewer.ViewModels;
namespace GalaxyViewer.Views;
@@ -10,12 +9,7 @@ public partial class LoginView : UserControl
public LoginView()
{
InitializeComponent();
var preferencesViewModel = new PreferencesViewModel();
DataContext = new LoginViewModel(
preferencesViewModel,
string.Empty,
string.Empty
);
DataContext = new LoginViewModel();
}
private void InitializeComponent()
+3 -3
View File
@@ -44,7 +44,7 @@
<MenuItem Header="Objects Nearby" />
</MenuItem>
<MenuItem Header="Communicate" IsEnabled="{Binding IsLoggedIn}">
<MenuItem Header="Chat" Command="{Binding ShowChatCommand}" />
<MenuItem Header="Chat" />
<MenuItem Header="Friends List" />
<MenuItem Header="Nearby People" />
<MenuItem Header="Voice" />
@@ -60,11 +60,11 @@
<MenuItem Header="Search" />
</MenuItem>
<MenuItem Header="Dev">
<MenuItem Header="Debug View" Command="{Binding ShowDevViewCommand}" />
<MenuItem Header="Debug View" />
</MenuItem>
<!-- Add more menu items as needed -->
</Menu>
<ContentControl x:Name="ContentControl" />
<ContentControl Content="{Binding CurrentView}"/>
</DockPanel>
</UserControl>
-12
View File
@@ -1,7 +1,5 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using GalaxyViewer.Services;
using GalaxyViewer.ViewModels;
namespace GalaxyViewer.Views;
@@ -10,16 +8,6 @@ public partial class MainView : UserControl
public MainView()
{
InitializeComponent();
var contentControl = this.FindControl<ContentControl>("ContentControl");
if (contentControl == null) return;
var navigationService = new NavigationService(contentControl);
// Register routes
navigationService.RegisterRoute("login", typeof(LoginView));
navigationService.RegisterRoute("debug", typeof(DebugView));
navigationService.RegisterRoute("preferences", typeof(PreferencesView));
DataContext = new MainViewModel();
}
private void InitializeComponent()
+55 -49
View File
@@ -4,54 +4,54 @@
"net8.0": {
"Avalonia": {
"type": "Direct",
"requested": "[11.1.4, )",
"resolved": "11.1.4",
"contentHash": "V1x2JtpY8PRL99iwtd5z7ltjE3AY0MbqFcQtt7ryCHr09S+LVTCjXj89P1enWJns6aa07cnJdjVUyyuXVCagYw==",
"requested": "[11.2.0, )",
"resolved": "11.2.0",
"contentHash": "dM5GvrBRNtxPDypOQ9TnUEx3zd5CmCkXUQX/kd2Ged2iqxHuJtipZcWiS5WtFfprlHa/J8ki4+0+jeawC0VtCA==",
"dependencies": {
"Avalonia.BuildServices": "0.0.29",
"Avalonia.Remote.Protocol": "11.1.4",
"Avalonia.Remote.Protocol": "11.2.0",
"MicroCom.Runtime": "0.11.0"
}
},
"Avalonia.Diagnostics": {
"type": "Direct",
"requested": "[11.1.4, )",
"resolved": "11.1.4",
"contentHash": "9eayqB1ZLO28zYEzUtKBSr3R31V6CJJOuMIC1MT8Tqh+Sy1dzdCP+161pwwsr0wUv7jBaztSttNFJeZW6lHLwg==",
"requested": "[11.2.0, )",
"resolved": "11.2.0",
"contentHash": "P0TSfq5sVdHuGC1xwuINkUAezMB92IxP+M/UBOEHWQRal13K4z2V8V75l9WH8ja0q/1B9sF6+4xcHS7JbOJhpQ==",
"dependencies": {
"Avalonia": "11.1.4",
"Avalonia.Controls.ColorPicker": "11.1.4",
"Avalonia.Controls.DataGrid": "11.1.4",
"Avalonia.Themes.Simple": "11.1.4"
"Avalonia": "11.2.0",
"Avalonia.Controls.ColorPicker": "11.2.0",
"Avalonia.Controls.DataGrid": "11.2.0",
"Avalonia.Themes.Simple": "11.2.0"
}
},
"Avalonia.Fonts.Inter": {
"type": "Direct",
"requested": "[11.1.4, )",
"resolved": "11.1.4",
"contentHash": "9ceNPUPG5kEIcTNRMPozYSTzM8Q9Vzi/gkXRzztH4dfvlNL2F2S8/mgOatz4zlfbgL1LphK56jATyHSts15/CA==",
"requested": "[11.2.0, )",
"resolved": "11.2.0",
"contentHash": "kLorc5wMPXer0xngTRFdXGb/Yq84xa4klI7m+7PW4mCSQ8dv3sRqBebecgSAs1/ADs9i/JTGdHgIPB0Yrec9ZA==",
"dependencies": {
"Avalonia": "11.1.4"
"Avalonia": "11.2.0"
}
},
"Avalonia.ReactiveUI": {
"type": "Direct",
"requested": "[11.1.4, )",
"resolved": "11.1.4",
"contentHash": "vGtbdBTPDvF/cJWMFN0V4Smp6YggAL2U2TSVFJfhr2PlOnIPOMEt2JPw9kYcbAlT4JZu5yFKds7tJRbQZK7d1w==",
"requested": "[11.2.0, )",
"resolved": "11.2.0",
"contentHash": "xecP1P3G7MzkXrwRIVmsfpQ8ofsS9XK3H4YXtyH+dZvqhXc/emsRGFfrtjL8TBidnE7JCLYP9QF7Z3n7bvMYSw==",
"dependencies": {
"Avalonia": "11.1.4",
"ReactiveUI": "18.3.1",
"System.Reactive": "5.0.0"
"Avalonia": "11.2.0",
"ReactiveUI": "20.1.1",
"System.Reactive": "6.0.1"
}
},
"Avalonia.Themes.Fluent": {
"type": "Direct",
"requested": "[11.1.4, )",
"resolved": "11.1.4",
"contentHash": "OcN5Kl+MHxc+ON0hRvOZFxLFSnMose1nFT3/UC5u09Qty5jRQCcGIlQbmIFCpG00ki+g2Gh+cNNt5jT+E349zw==",
"requested": "[11.2.0, )",
"resolved": "11.2.0",
"contentHash": "f0DgBcwCOdVChA10XfHIPBUUWeaG/r65zWpPynMgE8kPOyorJqsrbuGS//hxU4uRiQJwJgTGbQUqWVn3dEgM1g==",
"dependencies": {
"Avalonia": "11.1.4"
"Avalonia": "11.2.0"
}
},
"AvaloniaInside.Shell": {
@@ -167,33 +167,33 @@
},
"Avalonia.Controls.ColorPicker": {
"type": "Transitive",
"resolved": "11.1.4",
"contentHash": "eK1La32OEpqW7DALZN044endN2zOhKHMjtPE5pjMf77qMvITipt77bSINjmlGKTiHyQm7u7Eiw3LLzHrjenYng==",
"resolved": "11.2.0",
"contentHash": "sJTMhfF5j1mjgRIK4cKa8Ldg5cLI1IeFO4OVRlChiMQPTT5Mv6EURmrriCfJ7icP7opDgdAx1ogFWiLPYTjX0Q==",
"dependencies": {
"Avalonia": "11.1.4",
"Avalonia.Remote.Protocol": "11.1.4"
"Avalonia": "11.2.0",
"Avalonia.Remote.Protocol": "11.2.0"
}
},
"Avalonia.Controls.DataGrid": {
"type": "Transitive",
"resolved": "11.1.4",
"contentHash": "/Ly1U3HlTEd0aTMv9thbzQfRfNz7iaNZgJcLV63NkPBYGuyIffATksMBrZ0w6BfbP+zAM3NcACawaUx2VC9/1g==",
"resolved": "11.2.0",
"contentHash": "nmtigVPgYVYMZbQR1wRz5InSk5ab8INSwmFzDbIqsYdn+Mj4/5lB4d5MoxIj3wEQUcxqkO7iqNFsOKafYvjZqg==",
"dependencies": {
"Avalonia": "11.1.4",
"Avalonia.Remote.Protocol": "11.1.4"
"Avalonia": "11.2.0",
"Avalonia.Remote.Protocol": "11.2.0"
}
},
"Avalonia.Remote.Protocol": {
"type": "Transitive",
"resolved": "11.1.4",
"contentHash": "dkzCH6FO2qSaonBg1PA5ebQ5hS1zRGjrUjPUzuVpRivMjkfyi8MHKoI/MyhYNjAlkr87TuPoAfPkOEdthmMdYg=="
"resolved": "11.2.0",
"contentHash": "dt/YyfLV+WG+jrqwBIthL8UOX9Jmn1AQi9P3vrXyYrWpNJREYz7mEGTwPau5jNOTN2scitJ3fbxrzPO8yoAX0A=="
},
"Avalonia.Themes.Simple": {
"type": "Transitive",
"resolved": "11.1.4",
"contentHash": "NzQqXae8c5lHEHv3i3yRMYSFBYL0j8VNFPnG4rKqFlUV0bgX8zTOqB/UebP0nOxRcQJmzEjuYU200xT3aiaTxw==",
"resolved": "11.2.0",
"contentHash": "1unSXWE9wexRzsxypUUT3M+5Q8QTdvsVKh+M4cWB0Ps2drfA7wWSrO9/J1h8sWxky1OYr/VwKyar9Pl4AAcTrg==",
"dependencies": {
"Avalonia": "11.1.4"
"Avalonia": "11.2.0"
}
},
"CSJ2K.Skia": {
@@ -207,10 +207,10 @@
},
"DynamicData": {
"type": "Transitive",
"resolved": "7.9.5",
"contentHash": "xFwVha7o3qUtVYxco5p+7Urcztc/m1gmaEUxOG0i7LNe+vfCfyb0ECAsT2FLm3zOPHb0g8s9qVu5LfPKfRNVng==",
"resolved": "8.4.1",
"contentHash": "Mn1+fU/jqxgONEJq8KLQPGWEi7g/hUVTbjZyn4QM0sWWDAVOHPO9WjXWORSykwdfg/6S3GM15qsfz+2EvO+QAQ==",
"dependencies": {
"System.Reactive": "5.0.0"
"System.Reactive": "6.0.0"
}
},
"Irihi.Avalonia.Shared": {
@@ -373,11 +373,12 @@
},
"ReactiveUI": {
"type": "Transitive",
"resolved": "18.3.1",
"contentHash": "0tclGtjrRPfA2gbjiM7O3DeNmo6/TpDn7CMN6jgzDrbgrnysM7oEzjGEeXbtXaOxH6kEf6RiMKWobZoSgbBXhQ==",
"resolved": "20.1.1",
"contentHash": "9hNPknWjijnaSWs6auypoXqUptPZcRpUypF+cf1zD50fgW+SEoQda502N3fVZ2eWPcaiUad+z6GaLwOWmUVHNw==",
"dependencies": {
"DynamicData": "7.9.5",
"Splat": "14.4.1"
"DynamicData": "8.4.1",
"Splat": "15.1.1",
"System.ComponentModel.Annotations": "5.0.0"
}
},
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
@@ -567,8 +568,8 @@
},
"Splat": {
"type": "Transitive",
"resolved": "14.4.1",
"contentHash": "Z1Mncnzm9pNIaIbZ/EWH6x5ESnKsmAvu8HP4StBRw+yhz0lzE7LCbt22TNTPaFrYLYbYCbGQIc/61yuSnpLidg=="
"resolved": "15.1.1",
"contentHash": "RHDTdF90FwVbRia2cmuIzkiVoETqnXSB2dDBBi/I35HWXqv4OKGqoMcfcd6obMvO2OmmY5PjU1M62K8LkJafAA=="
},
"System.Buffers": {
"type": "Transitive",
@@ -602,6 +603,11 @@
"System.Threading.Tasks": "4.3.0"
}
},
"System.ComponentModel.Annotations": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg=="
},
"System.Configuration.ConfigurationManager": {
"type": "Transitive",
"resolved": "4.5.0",
@@ -789,8 +795,8 @@
},
"System.Reactive": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ=="
"resolved": "6.0.1",
"contentHash": "rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg=="
},
"System.Reflection": {
"type": "Transitive",