mirror of
https://github.com/GalaxyViewer/GalaxyViewer.git
synced 2026-08-14 09:02:08 +00:00
✨ 📦 🐛 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:
@@ -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,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
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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"];
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user