diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..4c9295f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: GalaxyViewer +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: galaxylittlepaws +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f33b7c5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog +All notable changes to this project will be documented in this file. + +## [Unreleased] 0.1.0 +- Added Login functionality +- Added Preferences functionality +- Added ability to choose language from a list of predefined languages +- Added ability to choose login location from a list of predefined locations +- Added ability to choose theme from a list of predefined themes +- Added ability to choose font from a list of predefined fonts diff --git a/Directory.Build.props b/Directory.Build.props index 17bc977..37fd070 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,9 +1,10 @@ - - 0.1.0 - GalaxyViewer - Galaxy Littlepaws - enable - 11.0.10 - + + 0.1.0 + GalaxyViewer + GalaxyViewer is a cross-platform viewer for Second Life and NGC OpenSimulator. + Galaxy Littlepaws + enable + 11.1.0 + \ No newline at end of file diff --git a/GalaxyViewer.Android/GalaxyViewer.Android.csproj b/GalaxyViewer.Android/GalaxyViewer.Android.csproj index f03c260..612fd18 100644 --- a/GalaxyViewer.Android/GalaxyViewer.Android.csproj +++ b/GalaxyViewer.Android/GalaxyViewer.Android.csproj @@ -1,28 +1,24 @@ - - Exe - net8.0-android - 21 - enable - com.GalaxyViewer.GalaxyViewer - 1 - 1.0 - apk - false - + + Exe + net8.0-android34.0 + true + 30 + 24.0 + enable + com.GalaxyViewer.GalaxyViewer + 1 + 1.0 + apk + false + - - - Resources\drawable\Icon.png - - + + + + - - - - - - - - + + + diff --git a/GalaxyViewer.Android/GalaxyViewer.Android.iml b/GalaxyViewer.Android/GalaxyViewer.Android.iml new file mode 100644 index 0000000..9be9bdb --- /dev/null +++ b/GalaxyViewer.Android/GalaxyViewer.Android.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/GalaxyViewer.Android/Icon.png b/GalaxyViewer.Android/Icon.png deleted file mode 100644 index 41a2a61..0000000 Binary files a/GalaxyViewer.Android/Icon.png and /dev/null differ diff --git a/GalaxyViewer.Android/MainActivity.cs b/GalaxyViewer.Android/MainActivity.cs index b4fd703..d5bb678 100644 --- a/GalaxyViewer.Android/MainActivity.cs +++ b/GalaxyViewer.Android/MainActivity.cs @@ -1,23 +1,74 @@ -using Android.App; +using Android; +using Android.App; using Android.Content.PM; +using Android.OS; +using Android.Widget; +using AndroidX.Core.App; +using AndroidX.Core.Content; using Avalonia; using Avalonia.Android; using Avalonia.ReactiveUI; +using GalaxyViewer.Services; -namespace GalaxyViewer.Android; - -[Activity( - Label = "GalaxyViewer.Android", - Theme = "@style/MyTheme.NoActionBar", - Icon = "@drawable/icon", - MainLauncher = true, - ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] -public class MainActivity : AvaloniaMainActivity +namespace GalaxyViewer.Android { - protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + [Activity( + Label = "GalaxyViewer.Android", + Theme = "@style/MyTheme.NoActionBar", + Icon = "@drawable/icon", + MainLauncher = true, + ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] + public class MainActivity : AvaloniaMainActivity { - return base.CustomizeAppBuilder(builder) - .WithInterFont() - .UseReactiveUI(); + const int RequestStorageId = 0; + + protected override void OnCreate(Bundle savedInstanceState) + { + base.OnCreate(savedInstanceState); + + if (ContextCompat.CheckSelfPermission(this, Manifest.Permission.WriteExternalStorage) != Permission.Granted) + { + ActivityCompat.RequestPermissions(this, new string[] { Manifest.Permission.WriteExternalStorage }, RequestStorageId); + } + else + { + InitializeLiteDbService(); + } + } + + public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) + { + base.OnRequestPermissionsResult(requestCode, permissions, grantResults); + + if (requestCode == RequestStorageId) + { + if (grantResults.Length > 0 && grantResults[0] == Permission.Granted) + { + // Permission granted, proceed with file operations + InitializeLiteDbService(); + } + else + { + // Permission denied, show a message to the user + var message = "Permission to create data storage file denied. The application will not be able to function."; + var toast = Toast.MakeText(this, message, ToastLength.Long); + toast.Show(); + } + } + } + + private void InitializeLiteDbService() + { + // Initialize LiteDbService here + var liteDbService = new LiteDbService(); + // Use liteDbService as needed + } + + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + { + return base.CustomizeAppBuilder(builder) + .WithInterFont() + .UseReactiveUI(); + } } -} +} \ No newline at end of file diff --git a/GalaxyViewer.Android/Properties/AndroidManifest.xml b/GalaxyViewer.Android/Properties/AndroidManifest.xml index f19bba0..e3a0233 100644 --- a/GalaxyViewer.Android/Properties/AndroidManifest.xml +++ b/GalaxyViewer.Android/Properties/AndroidManifest.xml @@ -1,5 +1,15 @@  - - - + + + + + + + + diff --git a/GalaxyViewer.Android/Resources/drawable/icon.png b/GalaxyViewer.Android/Resources/drawable/icon.png new file mode 100644 index 0000000..03b9417 Binary files /dev/null and b/GalaxyViewer.Android/Resources/drawable/icon.png differ diff --git a/GalaxyViewer.Android/Resources/values-night/colors.xml b/GalaxyViewer.Android/Resources/values-night/colors.xml index 3d47b6f..e727ff0 100644 --- a/GalaxyViewer.Android/Resources/values-night/colors.xml +++ b/GalaxyViewer.Android/Resources/values-night/colors.xml @@ -1,4 +1,4 @@  - #212121 + #0f1729 diff --git a/GalaxyViewer.Android/Resources/values/colors.xml b/GalaxyViewer.Android/Resources/values/colors.xml index 59279d5..e727ff0 100644 --- a/GalaxyViewer.Android/Resources/values/colors.xml +++ b/GalaxyViewer.Android/Resources/values/colors.xml @@ -1,4 +1,4 @@  - #FFFFFF + #0f1729 diff --git a/GalaxyViewer.Browser/GalaxyViewer.Browser.csproj b/GalaxyViewer.Browser/GalaxyViewer.Browser.csproj index ab16d45..ec18777 100644 --- a/GalaxyViewer.Browser/GalaxyViewer.Browser.csproj +++ b/GalaxyViewer.Browser/GalaxyViewer.Browser.csproj @@ -1,21 +1,21 @@  Exe - net8.0-browser + net8.0 browser-wasm - wwwroot\main.js + wwwroot/main.js ./_framework - + - + - + \ No newline at end of file diff --git a/GalaxyViewer.Desktop/GalaxyViewer.Desktop.csproj b/GalaxyViewer.Desktop/GalaxyViewer.Desktop.csproj index 081b729..4d7ebe5 100644 --- a/GalaxyViewer.Desktop/GalaxyViewer.Desktop.csproj +++ b/GalaxyViewer.Desktop/GalaxyViewer.Desktop.csproj @@ -13,9 +13,9 @@ - + - + diff --git a/GalaxyViewer.sln.DotSettings b/GalaxyViewer.sln.DotSettings index 29d1099..317d147 100644 --- a/GalaxyViewer.sln.DotSettings +++ b/GalaxyViewer.sln.DotSettings @@ -1,2 +1,5 @@  + False + False + True True \ No newline at end of file diff --git a/GalaxyViewer/App.axaml b/GalaxyViewer/App.axaml index 3e557b5..fb4985b 100644 --- a/GalaxyViewer/App.axaml +++ b/GalaxyViewer/App.axaml @@ -2,7 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:GalaxyViewer" xmlns:semi="https://irihi.tech/semi" - xmlns:u-semi="https://irihi.tech/ursa/themes/semi" + xmlns:u-Semi="https://irihi.tech/ursa/themes/semi" + xmlns:converters="clr-namespace:GalaxyViewer.Converters" x:Class="GalaxyViewer.App"> @@ -10,7 +11,7 @@ - + @@ -18,6 +19,7 @@ + \ No newline at end of file diff --git a/GalaxyViewer/App.axaml.cs b/GalaxyViewer/App.axaml.cs index 5e32e54..d00e80d 100644 --- a/GalaxyViewer/App.axaml.cs +++ b/GalaxyViewer/App.axaml.cs @@ -1,4 +1,6 @@ +using System; using System.Diagnostics; +using System.IO; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; @@ -6,96 +8,134 @@ 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; } + private static void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + // Register other services here + } public override void Initialize() { - PreferencesManager = new PreferencesManager(); + var serviceCollection = new ServiceCollection(); + ConfigureServices(serviceCollection); + _serviceProvider = serviceCollection.BuildServiceProvider(); + + var liteDbService = _serviceProvider.GetService(); + if (liteDbService == null) + { + throw new InvalidOperationException("LiteDbService is not registered."); + } + + PreferencesManager = new PreferencesManager(liteDbService); PreferencesManager.PreferencesChanged += OnPreferencesChanged; + AvaloniaXamlLoader.Load(this); base.Initialize(); } - public override async void OnFrameworkInitializationCompleted() + public override void OnFrameworkInitializationCompleted() { - var contentControl = new ContentControl(); - var navigationService = new NavigationService(contentControl); - - // Register routes - navigationService.RegisterRoute("login", typeof(LoginView)); - navigationService.RegisterRoute("debug", typeof(DebugView)); - navigationService.RegisterRoute("preferences", typeof(PreferencesView)); - - switch (ApplicationLifetime) + try { - case IClassicDesktopStyleApplicationLifetime desktop: - desktop.MainWindow = new MainWindow - { - DataContext = new MainViewModel(navigationService) - }; - break; - case ISingleViewApplicationLifetime singleViewPlatform: - singleViewPlatform.MainView = new MainView - { - DataContext = new MainViewModel(navigationService) - }; - break; + 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 } - - Debug.Assert(PreferencesManager != null, nameof(PreferencesManager) + " != null"); - var preferences = await PreferencesManager.LoadPreferencesAsync(); - ApplyPreferences(preferences); base.OnFrameworkInitializationCompleted(); } - private async void OnPreferencesChanged(object? sender, PreferencesModel preferences) + 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(); } } \ No newline at end of file diff --git a/GalaxyViewer/Assets/GalaxyViewerLogo.ico b/GalaxyViewer/Assets/GalaxyViewerLogo.ico new file mode 100644 index 0000000..74a3c6c Binary files /dev/null and b/GalaxyViewer/Assets/GalaxyViewerLogo.ico differ diff --git a/GalaxyViewer/Assets/Images/Logo.png b/GalaxyViewer/Assets/Images/Logo.png new file mode 100644 index 0000000..03b9417 Binary files /dev/null and b/GalaxyViewer/Assets/Images/Logo.png differ diff --git a/GalaxyViewer/Assets/Images/Logo.svg b/GalaxyViewer/Assets/Images/Logo.svg new file mode 100644 index 0000000..97b9d96 --- /dev/null +++ b/GalaxyViewer/Assets/Images/Logo.svg @@ -0,0 +1,119 @@ + + + + + + diff --git a/GalaxyViewer/Assets/Localization/LocalizationManager.cs b/GalaxyViewer/Assets/Localization/LocalizationManager.cs index b6fc5bd..e38578e 100644 --- a/GalaxyViewer/Assets/Localization/LocalizationManager.cs +++ b/GalaxyViewer/Assets/Localization/LocalizationManager.cs @@ -2,30 +2,29 @@ using System.Globalization; using System.Resources; -namespace GalaxyViewer.Assets.Localization +namespace GalaxyViewer.Assets.Localization; + +public class LocalizationManager : INotifyPropertyChanged { - public class LocalizationManager : INotifyPropertyChanged + private readonly ResourceManager _resourceManager = new(typeof(Strings)); + private CultureInfo _currentCulture = CultureInfo.CurrentCulture; + + public event PropertyChangedEventHandler? PropertyChanged; + + public string GetString(string name) { - private readonly ResourceManager _resourceManager = new(typeof(Strings)); - private CultureInfo _currentCulture = CultureInfo.CurrentCulture; + return _resourceManager.GetString(name, _currentCulture) ?? $"[{name}]"; + } - public event PropertyChangedEventHandler? PropertyChanged; - - public string GetString(string name) + public void SetCulture(string cultureCode) + { + var newCulture = new CultureInfo(cultureCode); + if (_currentCulture.Name != newCulture.Name) { - return _resourceManager.GetString(name, _currentCulture) ?? $"[{name}]"; - } - - public void SetCulture(string cultureCode) - { - var newCulture = new CultureInfo(cultureCode); - if (_currentCulture.Name != newCulture.Name) - { - _currentCulture = newCulture; - CultureInfo.CurrentCulture = newCulture; - CultureInfo.CurrentUICulture = newCulture; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null)); - } + _currentCulture = newCulture; + CultureInfo.CurrentCulture = newCulture; + CultureInfo.CurrentUICulture = newCulture; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null)); } } } \ No newline at end of file diff --git a/GalaxyViewer/Assets/Localization/Strings.en-US.resx b/GalaxyViewer/Assets/Localization/Strings.en-US.resx index e8d3471..3d14c34 100644 --- a/GalaxyViewer/Assets/Localization/Strings.en-US.resx +++ b/GalaxyViewer/Assets/Localization/Strings.en-US.resx @@ -25,12 +25,6 @@ - - GalaxyViewer - - - 0.1 - Welcome to GalaxyViewer 🌌 diff --git a/GalaxyViewer/Assets/Localization/Strings.resx b/GalaxyViewer/Assets/Localization/Strings.resx index 71a4efe..54388f7 100644 --- a/GalaxyViewer/Assets/Localization/Strings.resx +++ b/GalaxyViewer/Assets/Localization/Strings.resx @@ -23,12 +23,6 @@ This is in English (United States) language, as the default. It is in case there System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - GalaxyViewer - - - 0.1 - Welcome to GalaxyViewer 🌌 diff --git a/GalaxyViewer/Assets/grids.xml b/GalaxyViewer/Assets/grids.xml deleted file mode 100644 index f08a750..0000000 --- a/GalaxyViewer/Assets/grids.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - gridnick - agni - gridname - Second Life (agni) - platform - SecondLife - loginuri - https://login.agni.lindenlab.com/cgi-bin/login.cgi - loginpage - http://secondlife.com/app/login/?channel=Second+Life+Release - helperuri - https://secondlife.com/helpers/ - website - http://secondlife.com/ - support - http://secondlife.com/support/ - register - http://secondlife.com/registration/ - password - http://secondlife.com/account/request.php - version - 0 - - - - - gridnick - aditi - gridname - Second Life Beta (aditi) - platform - SecondLife - loginuri - https://login.aditi.lindenlab.com/cgi-bin/login.cgi - loginpage - http://secondlife.com/app/login/?channel=Second+Life+Beta - helperuri - http://aditi-secondlife.webdev.lindenlab.com/helpers/ - website - http://secondlife.com/ - support - http://secondlife.com/support/ - register - http://secondlife.com/registration/ - password - http://secondlife.com/account/request.php - version - 1 - - - - \ No newline at end of file diff --git a/GalaxyViewer/Assets/preferences.xml b/GalaxyViewer/Assets/preferences.xml deleted file mode 100644 index e5bcc7a..0000000 --- a/GalaxyViewer/Assets/preferences.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - Default - Home - Atkinson Hyperlegible - en-US - 1721172337 - \ No newline at end of file diff --git a/GalaxyViewer/Commands/RelayCommand.cs b/GalaxyViewer/Commands/RelayCommand.cs index e81a879..01e1fdc 100644 --- a/GalaxyViewer/Commands/RelayCommand.cs +++ b/GalaxyViewer/Commands/RelayCommand.cs @@ -2,27 +2,26 @@ using System.Threading.Tasks; using System.Windows.Input; -namespace GalaxyViewer.Commands +namespace GalaxyViewer.Commands; + +public class RelayCommand(Func execute, Func? canExecute = null) + : ICommand { - public class RelayCommand(Func execute, Func? canExecute = null) - : ICommand + private readonly Func _execute = + execute ?? throw new ArgumentNullException(nameof(execute)); + + public bool CanExecute(object? parameter) => canExecute?.Invoke() ?? true; + + public async void Execute(object? parameter) => await _execute(); + + public event EventHandler? CanExecuteChanged { - private readonly Func _execute = - execute ?? throw new ArgumentNullException(nameof(execute)); + add => CommandManager.RequerySuggested += value; + remove => CommandManager.RequerySuggested -= value; + } - public bool CanExecute(object? parameter) => canExecute?.Invoke() ?? true; - - public async void Execute(object? parameter) => await _execute(); - - public event EventHandler? CanExecuteChanged - { - add => CommandManager.RequerySuggested += value; - remove => CommandManager.RequerySuggested -= value; - } - - private abstract class CommandManager - { - public static EventHandler? RequerySuggested { get; set; } - } + private abstract class CommandManager + { + public static EventHandler? RequerySuggested { get; set; } } } \ No newline at end of file diff --git a/GalaxyViewer/Controls/EmojiTextBlock.cs b/GalaxyViewer/Controls/EmojiTextBlock.cs index 8f9d018..e6820c8 100644 --- a/GalaxyViewer/Controls/EmojiTextBlock.cs +++ b/GalaxyViewer/Controls/EmojiTextBlock.cs @@ -5,53 +5,52 @@ using Avalonia.Controls; using Avalonia.Controls.Documents; using Avalonia.Media; -namespace GalaxyViewer.Controls +namespace GalaxyViewer.Controls; + +public abstract partial class EmojiTextBlock : TextBlock { - public abstract partial class EmojiTextBlock : TextBlock + private static readonly Regex EmojiRegex = MyRegex(); + + public static readonly StyledProperty EmojiFontFamilyProperty = + AvaloniaProperty.Register(nameof(EmojiFontFamily)); + + public FontFamily EmojiFontFamily { - private static readonly Regex EmojiRegex = MyRegex(); - - public static readonly StyledProperty EmojiFontFamilyProperty = - AvaloniaProperty.Register(nameof(EmojiFontFamily)); - - public FontFamily EmojiFontFamily - { - get => GetValue(EmojiFontFamilyProperty); - set => SetValue(EmojiFontFamilyProperty, value); - } - - public static readonly StyledProperty DefaultFontFamilyProperty = - AvaloniaProperty.Register(nameof(DefaultFontFamily)); - - public FontFamily DefaultFontFamily - { - get => GetValue(DefaultFontFamilyProperty); - set => SetValue(DefaultFontFamilyProperty, value); - } - - protected EmojiTextBlock() - { - this.PropertyChanged += (_, e) => - { - if (e.Property == TextProperty) - { - ApplyFonts(); - } - }; - } - - private void ApplyFonts() - { - if (string.IsNullOrEmpty(Text)) - return; - - var inlines = Text.Select(ch => new Run(ch.ToString()) { FontFamily = EmojiRegex.IsMatch(ch.ToString()) ? EmojiFontFamily : DefaultFontFamily }).Cast().ToList(); - - Inlines?.Clear(); - Inlines?.AddRange(inlines); - } - - [GeneratedRegex(@"[\u203C-\u3299\u1F000-\u1F644\u1F680-\u1F6FF\u1F700-\u1F77F\u1F780-\u1F7FF\u1F800-\u1F8FF\u1F900-\u1F9FF\u1FA00-\u1FA6F\u1FA70-\u1FAFF\u1FB00-\u1FBFF]", RegexOptions.Compiled)] - private static partial Regex MyRegex(); + get => GetValue(EmojiFontFamilyProperty); + set => SetValue(EmojiFontFamilyProperty, value); } + + public static readonly StyledProperty DefaultFontFamilyProperty = + AvaloniaProperty.Register(nameof(DefaultFontFamily)); + + public FontFamily DefaultFontFamily + { + get => GetValue(DefaultFontFamilyProperty); + set => SetValue(DefaultFontFamilyProperty, value); + } + + protected EmojiTextBlock() + { + this.PropertyChanged += (_, e) => + { + if (e.Property == TextProperty) + { + ApplyFonts(); + } + }; + } + + private void ApplyFonts() + { + if (string.IsNullOrEmpty(Text)) + return; + + var inlines = Text.Select(ch => new Run(ch.ToString()) { FontFamily = EmojiRegex.IsMatch(ch.ToString()) ? EmojiFontFamily : DefaultFontFamily }).Cast().ToList(); + + Inlines?.Clear(); + Inlines?.AddRange(inlines); + } + + [GeneratedRegex(@"[\u203C-\u3299\u1F000-\u1F644\u1F680-\u1F6FF\u1F700-\u1F77F\u1F780-\u1F7FF\u1F800-\u1F8FF\u1F900-\u1F9FF\u1FA00-\u1FA6F\u1FA70-\u1FAFF\u1FB00-\u1FBFF]", RegexOptions.Compiled)] + private static partial Regex MyRegex(); } \ No newline at end of file diff --git a/GalaxyViewer/GalaxyViewer.csproj b/GalaxyViewer/GalaxyViewer.csproj index 41c26ac..b79e0da 100644 --- a/GalaxyViewer/GalaxyViewer.csproj +++ b/GalaxyViewer/GalaxyViewer.csproj @@ -10,39 +10,41 @@ - - + + PreserveNewest PreserveNewest - + - + - - - - + + + + - + - - - + + + + + - - + + @@ -72,6 +74,6 @@ - + diff --git a/GalaxyViewer/Models/DebugViewModel.cs b/GalaxyViewer/Models/DebugViewModel.cs new file mode 100644 index 0000000..f793495 --- /dev/null +++ b/GalaxyViewer/Models/DebugViewModel.cs @@ -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; + } +} \ No newline at end of file diff --git a/GalaxyViewer/Models/GridModel.cs b/GalaxyViewer/Models/GridModel.cs index af48ec9..b67ed00 100644 --- a/GalaxyViewer/Models/GridModel.cs +++ b/GalaxyViewer/Models/GridModel.cs @@ -1,9 +1,10 @@ -using System; +using LiteDB; namespace GalaxyViewer.Models { - public class Grid + public class GridModel { + [BsonId] public ObjectId Id { get; set; } public string GridNick { get; set; } public string GridName { get; set; } public string Platform { get; set; } @@ -15,20 +16,5 @@ namespace GalaxyViewer.Models public string Register { get; set; } public string Password { get; set; } public string Version { get; set; } - - public Grid(string gridNick, string gridName, string platform, string loginUri, string loginPage, string helperUri, string website, string support, string register, string password, string version) - { - GridNick = !string.IsNullOrEmpty(gridNick) ? gridNick : throw new ArgumentNullException(nameof(gridNick)); - GridName = !string.IsNullOrEmpty(gridName) ? gridName : throw new ArgumentNullException(nameof(gridName)); - Platform = !string.IsNullOrEmpty(platform) ? platform : throw new ArgumentNullException(nameof(platform)); - LoginUri = !string.IsNullOrEmpty(loginUri) ? loginUri : throw new ArgumentNullException(nameof(loginUri)); - LoginPage = !string.IsNullOrEmpty(loginPage) ? loginPage : throw new ArgumentNullException(nameof(loginPage)); - HelperUri = !string.IsNullOrEmpty(helperUri) ? helperUri : throw new ArgumentNullException(nameof(helperUri)); - Website = !string.IsNullOrEmpty(website) ? website : throw new ArgumentNullException(nameof(website)); - Support = !string.IsNullOrEmpty(support) ? support : throw new ArgumentNullException(nameof(support)); - Register = !string.IsNullOrEmpty(register) ? register : throw new ArgumentNullException(nameof(register)); - Password = !string.IsNullOrEmpty(password) ? password : throw new ArgumentNullException(nameof(password)); - Version = !string.IsNullOrEmpty(version) ? version : throw new ArgumentNullException(nameof(version)); - } } } \ No newline at end of file diff --git a/GalaxyViewer/Models/PreferencesModel.cs b/GalaxyViewer/Models/PreferencesModel.cs index 00f1d15..48ca3ec 100644 --- a/GalaxyViewer/Models/PreferencesModel.cs +++ b/GalaxyViewer/Models/PreferencesModel.cs @@ -1,85 +1,16 @@ -using System; using System.Collections.Generic; -using System.Xml.Serialization; +using LiteDB; namespace GalaxyViewer.Models { - [Serializable] public class PreferencesModel { - public PreferencesModel() - { - ThemeOptions = new List { "Light", "Dark", "Default" }; - LoginLocationOptions = new List { "Home", "Last Location" }; - FontOptions = new List { "Inter", "Atkinson Hyperlegible" }; - LanguageOptions = new List { "en-US" }; - } - - [XmlIgnore] - public List ThemeOptions { get; set; } - - [XmlIgnore] - public List LoginLocationOptions { get; set; } - - [XmlIgnore] - public List FontOptions { get; set; } - - [XmlIgnore] - public List LanguageOptions { get; set; } - - // Default values - private string _theme = "Default"; - public string Theme - { - get => _theme; - set - { - if (_theme != value) - { - _theme = value; - } - } - } - - private string _loginLocation = "Home"; - public string LoginLocation - { - get => _loginLocation; - set - { - if (_loginLocation != value) - { - _loginLocation = value; - } - } - } - - private string _font = "Atkinson Hyperlegible"; - public string Font - { - get => _font; - set - { - if (_font != value) - { - _font = value; - } - } - } - - private string _language = "en-US"; - public string Language - { - get => _language; - set - { - if (_language != value) - { - _language = value; - } - } - } - + [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 string SelectedGridNick { get; set; } public long LastSavedEpoch { get; set; } } } \ No newline at end of file diff --git a/GalaxyViewer/Models/PreferencesOptions.cs b/GalaxyViewer/Models/PreferencesOptions.cs new file mode 100644 index 0000000..f938e9f --- /dev/null +++ b/GalaxyViewer/Models/PreferencesOptions.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace GalaxyViewer.Models; + +public static class PreferencesOptions +{ + public static readonly List ThemeOptions = ["Light", "Dark", "Default"]; + public static readonly List LoginLocationOptions = ["Home", "Last Location"]; + public static readonly List FontOptions = ["Inter", "Atkinson Hyperlegible"]; + public static readonly List LanguageOptions = ["en-US"]; +} \ No newline at end of file diff --git a/GalaxyViewer/Services/GridService.cs b/GalaxyViewer/Services/GridService.cs index a3ba748..b5c6a5f 100644 --- a/GalaxyViewer/Services/GridService.cs +++ b/GalaxyViewer/Services/GridService.cs @@ -1,40 +1,43 @@ using System; using System.Collections.Generic; using System.IO; -using System.Xml.Linq; +using System.Linq; +using LiteDB; using GalaxyViewer.Models; namespace GalaxyViewer.Services { public class GridService { - public List ParseGridsFromXml() + private readonly string _databasePath; + + public GridService() { - var xmlFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets", "grids.xml"); - var xDoc = XDocument.Load(xmlFilePath); - var grids = new List(); + var appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GalaxyViewer"); + Directory.CreateDirectory(appDataPath); // Ensure the directory exists + _databasePath = Path.Combine(appDataPath, "data.db"); + } - if (xDoc.Root == null) return grids; - foreach (var gridElement in xDoc.Root.Elements("grid")) + public List GetAllGrids() + { + try { - var grid = new Grid( - gridElement.Element("gridnick")?.Value ?? string.Empty, - gridElement.Element("gridname")?.Value ?? string.Empty, - gridElement.Element("platform")?.Value ?? string.Empty, - gridElement.Element("loginuri")?.Value ?? string.Empty, - gridElement.Element("loginpage")?.Value ?? string.Empty, - gridElement.Element("helperuri")?.Value ?? string.Empty, - gridElement.Element("website")?.Value ?? string.Empty, - gridElement.Element("support")?.Value ?? string.Empty, - gridElement.Element("register")?.Value ?? string.Empty, - gridElement.Element("password")?.Value ?? string.Empty, - gridElement.Element("version")?.Value ?? string.Empty - ); - - grids.Add(grid); + using var db = new LiteDatabase(_databasePath); + var collection = db.GetCollection("grids"); + return collection.FindAll().ToList(); + } + catch (IOException ex) when (ex.Message.Contains("Read-only file system")) + { + // Handle read-only file system scenario + Console.Error.WriteLine("Error: The file system is read-only. Please check the file system permissions."); + return []; + } + catch (Exception ex) + { + // Handle other exceptions + Console.Error.WriteLine($"An error occurred: {ex.Message}"); + throw; } - - return grids; } } } \ No newline at end of file diff --git a/GalaxyViewer/Services/LiteDbService.cs b/GalaxyViewer/Services/LiteDbService.cs new file mode 100644 index 0000000..b6d2b69 --- /dev/null +++ b/GalaxyViewer/Services/LiteDbService.cs @@ -0,0 +1,158 @@ +using LiteDB; +using System; +using System.IO; +using GalaxyViewer.Models; +using Serilog; + +namespace GalaxyViewer.Services +{ + public class LiteDbService : IDisposable + { + private LiteDatabase? _database; + private readonly string _databasePath; + + public LiteDbService() + { + try + { + _databasePath = GetDatabasePath(); + Directory.CreateDirectory(Path.GetDirectoryName(_databasePath) ?? throw new InvalidOperationException()); // Ensure the directory exists + _database = new LiteDatabase(_databasePath); + Log.Information("LiteDbService initialized with database path: {DbPath}", _databasePath); + + // Call SeedDatabase to ensure the grids table is generated + SeedDatabase(); + } + catch (LiteException ex) + { + Log.Error(ex, "LiteDB exception occurred. Attempting to recreate the database."); + HandleDatabaseCorruption(); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to initialize LiteDbService"); + throw; + } + } + + private static string GetDatabasePath() + { + string appDataPath; + appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "GalaxyViewer"); + return Path.Combine(appDataPath, "data.db"); + } + + private void HandleDatabaseCorruption() + { + try + { + if (File.Exists(_databasePath)) + { + File.Delete(_databasePath); + } + _database = new LiteDatabase(_databasePath); + SeedDatabase(); + Log.Information("Database recreated successfully."); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to recreate the database."); + throw; + } + } + + public ILiteCollection GetCollection(string name) + { + Log.Information("Retrieving collection: {CollectionName}", name); + return _database.GetCollection(name); + } + + public LiteDatabase? Database() + { + return _database; + } + + private void SeedDatabase() + { + SeedPreferences(); + SeedGrids(); + } + + private void SeedPreferences() + { + try + { + var preferencesCollection = _database.GetCollection("preferences"); + if (preferencesCollection.Count() != 0) return; + var defaultPreferences = new PreferencesModel + { + Theme = "Default", + LoginLocation = "Home", + Font = "Atkinson Hyperlegible", + Language = "en-US", + SelectedGridNick = "agni", + LastSavedEpoch = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + }; + preferencesCollection.Insert(defaultPreferences); + Log.Information("Database seeded with default preferences"); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to seed preferences"); + } + } + + private void SeedGrids() + { + try + { + var gridsCollection = _database.GetCollection("grids"); + if (gridsCollection.Count() != 0) return; + var grids = new[] + { + new GridModel + { + GridNick = "agni", + GridName = "Second Life (agni)", + Platform = "SecondLife", + LoginUri = "https://login.agni.lindenlab.com/cgi-bin/login.cgi", + LoginPage = "http://secondlife.com/app/login/?channel=Second+Life+Release", + HelperUri = "https://secondlife.com/helpers/", + Website = "http://secondlife.com/", + Support = "http://secondlife.com/support/", + Register = "http://secondlife.com/registration/", + Password = "http://secondlife.com/account/request.php", + Version = "0" + }, + new GridModel + { + GridNick = "aditi", + GridName = "Second Life Beta (aditi)", + Platform = "SecondLife", + LoginUri = "https://login.aditi.lindenlab.com/cgi-bin/login.cgi", + LoginPage = "http://secondlife.com/app/login/?channel=Second+Life+Beta", + HelperUri = "http://aditi-secondlife.webdev.lindenlab.com/helpers/", + Website = "http://secondlife.com/", + Support = "http://secondlife.com/support/", + Register = "http://secondlife.com/registration/", + Password = "http://secondlife.com/account/request.php", + Version = "1" + } + }; + + gridsCollection.InsertBulk(grids); + Log.Information("Database seeded with default grids"); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to seed grids"); + } + } + + public void Dispose() + { + _database?.Dispose(); + Log.Information("LiteDbService disposed"); + } + } +} \ No newline at end of file diff --git a/GalaxyViewer/Services/NavigationService.cs b/GalaxyViewer/Services/NavigationService.cs index b016d98..9cace2b 100644 --- a/GalaxyViewer/Services/NavigationService.cs +++ b/GalaxyViewer/Services/NavigationService.cs @@ -2,34 +2,33 @@ using System.Collections.Generic; using Avalonia.Controls; -namespace GalaxyViewer.Services +namespace GalaxyViewer.Services; + +public class NavigationService { - public class NavigationService + private readonly Dictionary _routes = new(); + private readonly ContentControl _contentControl; + + public NavigationService(ContentControl contentControl) { - private readonly Dictionary _routes = new Dictionary(); - private readonly ContentControl _contentControl; + _contentControl = contentControl; + } - public NavigationService(ContentControl contentControl) + public void RegisterRoute(string uri, Type viewType) + { + _routes[uri] = viewType; + } + + public void NavigateTo(string uri) + { + if (_routes.TryGetValue(uri, out var viewType)) { - _contentControl = contentControl; + var view = (Control)Activator.CreateInstance(viewType)!; + _contentControl.Content = view; } - - public void RegisterRoute(string uri, Type viewType) + else { - _routes[uri] = viewType; - } - - public void Navigate(string uri) - { - if (_routes.TryGetValue(uri, out var viewType)) - { - var view = (Control)Activator.CreateInstance(viewType)!; - _contentControl.Content = view; - } - else - { - throw new InvalidOperationException($"No view registered for URI: {uri}"); - } + throw new InvalidOperationException($"No view registered for URI: {uri}"); } } } \ No newline at end of file diff --git a/GalaxyViewer/Services/PreferencesManager.cs b/GalaxyViewer/Services/PreferencesManager.cs index 9d4b959..600bbfc 100644 --- a/GalaxyViewer/Services/PreferencesManager.cs +++ b/GalaxyViewer/Services/PreferencesManager.cs @@ -1,77 +1,95 @@ using System; -using System.IO; -using System.Threading.Tasks; -using System.Xml.Serialization; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +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 _preferencesCollection; + private readonly ILiteCollection? _gridsCollection; - private bool _isLoadingPreferences; - private PreferencesModel _currentPreferences; - public PreferencesModel CurrentPreferences => _currentPreferences; + public event EventHandler? PreferencesChanged; - public PreferencesManager() + public PreferencesManager(LiteDbService? liteDbService) { - EnsurePreferencesDirectory(); + Debug.Assert(liteDbService != null, nameof(liteDbService) + " != null"); + var database = liteDbService.Database(); + Debug.Assert(database != null, nameof(database) + " != null"); + _preferencesCollection = database.GetCollection("preferences"); + _gridsCollection = database?.GetCollection("grids"); } - private void EnsurePreferencesDirectory() + public PreferencesModel CurrentPreferences { - var directoryPath = Path.GetDirectoryName(_preferencesFilePath); - if (!string.IsNullOrEmpty(directoryPath)) + get { - Directory.CreateDirectory(directoryPath); + 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(), + SelectedGridNick = string.Empty + }; } } public async Task 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", + SelectedGridNick = "agni", + LastSavedEpoch = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + }; + }); } - public bool IsLoadingPreferences => _isLoadingPreferences; - - public event EventHandler? 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) + _preferencesCollection.Upsert(preferences); + OnPreferencesChanged(preferences); + }); + } + + public Dictionary> GetCurrentPreferencesOptions() + { + return new Dictionary> { - Log.Error(ex, "Failed to save preferences"); - } + { "ThemeOptions", PreferencesOptions.ThemeOptions }, + { "LoginLocationOptions", PreferencesOptions.LoginLocationOptions }, + { "FontOptions", PreferencesOptions.FontOptions }, + { "LanguageOptions", PreferencesOptions.LanguageOptions } + }; + } + + public List GetGridOptions() + { + return _gridsCollection?.FindAll().Select(grid => grid.GridNick).ToList() ?? + []; + } + + private void OnPreferencesChanged(PreferencesModel preferences) + { + PreferencesChanged?.Invoke(this, preferences); } } } \ No newline at end of file diff --git a/GalaxyViewer/ViewModels/LoginViewModel.cs b/GalaxyViewer/ViewModels/LoginViewModel.cs index ce9be40..7a53b2d 100644 --- a/GalaxyViewer/ViewModels/LoginViewModel.cs +++ b/GalaxyViewer/ViewModels/LoginViewModel.cs @@ -1,67 +1,283 @@ -using System.Threading.Tasks; -using System.Text.RegularExpressions; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Reactive; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia.Controls.Notifications; +using GalaxyViewer.Models; +using GalaxyViewer.Services; using ReactiveUI; -using System.ComponentModel; -using GalaxyViewer.Assets.Localization; +using Ursa.Controls; using Serilog; using OpenMetaverse; -namespace GalaxyViewer.ViewModels; - -public abstract partial class LoginViewModel : ReactiveObject +namespace GalaxyViewer.ViewModels { - private string _username; - private string _password; - private readonly GridClient _client = new GridClient(); - private readonly LocalizationManager _localizationManager; - - protected LoginViewModel(string username, string password, - LocalizationManager localizationManager) + public class LoginViewModel : ReactiveObject, IRoutableViewModel { - _username = username; - _password = password; - _localizationManager = localizationManager; - ReactiveCommand.Create(TryLoginAsync); - } + public string UrlPathSegment => "login"; - public string Username - { - get => _username; - set => this.RaiseAndSetIfChanged(ref _username, value); - } - - public string Password - { - get => _password; - set => this.RaiseAndSetIfChanged(ref _password, value); - } - - private static async void TryLoginAsync() - { - /*var loginParams = new LoginParams( - _client, - MyRegex().Split(Username.Trim())[0], // firstName - MyRegex().Split(Username.Trim()).Length > 1 - ? MyRegex().Split(Username.Trim())[1] - : "Resident", // lastName - Password, - ViewerName, - ViewerVersion, - DetermineGridUrl()) + public IScreen HostScreen { - LoginLocation = DetermineStartLocation(), - AgreeToTos = true // Assuming you have a way to get this value - }; - - var loginSuccess = await Task.Run(() => _client.Network.Login(loginParams)); - - if (loginSuccess) - { - Log.Information(LoginSuccess); + get + { + Debug.Assert(_routableViewModelImplementation?.HostScreen != null, + "_routableViewModelImplementation?.HostScreen != null"); + return _routableViewModelImplementation.HostScreen; + } } - else + + private readonly PreferencesViewModel _preferencesViewModel; + private string _username; + private string _password; + private readonly GridClient _client = new(); + private IRoutableViewModel? _routableViewModelImplementation; + private readonly GridService _gridService; + private ObservableCollection _grids; + private GridModel _selectedGrid; + + private const string DefaultGridUri = + "https://login.agni.lindenlab.com/cgi-bin/login.cgi"; + + public WindowToastManager? ToastManager { get; set; } + + public LoginViewModel() { - Log.Logger.Error(LoginFailed); - }*/ + _preferencesViewModel = new PreferencesViewModel(); + _username = string.Empty; + _password = string.Empty; + LoginLocations = _preferencesViewModel.LoginLocationOptions; + SelectedLoginLocation = _preferencesViewModel.SelectedLoginLocation; + TryLoginCommand = ReactiveCommand.CreateFromTask(TryLoginAsync); + _gridService = new GridService(); + _grids = new ObservableCollection(); + + LoadGrids(); + + // 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."); + }); + + // Subscribe to the Network.LoginProgress event + _client.Network.LoginProgress += OnLoginProgress; + } + + public ObservableCollection 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 string LoginStatusMessage { get; set; } + + public ObservableCollection Grids { get; set; } + + public GridModel? SelectedGrid + { + get + { + var selectedGrid = _grids.FirstOrDefault(g => + g.GridNick == _preferencesViewModel.SelectedGridNick); + if (selectedGrid == null) + { + selectedGrid = new GridModel + { + GridNick = "Second Life", + LoginUri = DefaultGridUri + }; + } + + return selectedGrid; + } + set + { + if (value == null) return; + _preferencesViewModel.SelectedGridNick = value.GridNick; + this.RaiseAndSetIfChanged(ref _selectedGrid, value); + } + } + + private void LoadGrids() + { + var grids = _gridService.GetAllGrids(); + _grids = new ObservableCollection(grids); + SelectedGrid = _grids.FirstOrDefault(g => + g.GridNick == _preferencesViewModel.SelectedGridNick); + } + + public ReactiveCommand 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.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 + ); + + if (loginParams == null) + { + Log.Error("Failed to create login parameters"); + await ShowLoginErrorAsync("Failed to create login parameters"); + return; + } + + loginParams.URI = + SelectedGrid?.LoginUri; // Set the login URI to the selected grid's URI + 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) ?? false); + + if (loginSuccess) + { + Log.Information("Login successful"); + await ProcessCapabilitiesAsync(); + } + else if (_client?.Network?.LoginMessage.Contains("MFA required") == true) + { + Log.Warning("MFA required"); + var mfaCode = await ShowMfaInputDialogAsync(); + if (!string.IsNullOrEmpty(mfaCode)) + { + loginParams.MfaHash = Utils.MD5(mfaCode); + loginSuccess = + await Task.Run(() => _client?.Network?.Login(loginParams) ?? false); + if (loginSuccess) + { + Log.Information("Login successful with MFA"); + await ProcessCapabilitiesAsync(); + } + else + { + Log.Error("Login failed with MFA: {Error}", _client?.Network?.LoginMessage); + await ShowLoginErrorAsync( + $"Login failed with MFA: {_client?.Network?.LoginMessage}"); + } + } + else + { + Log.Warning("MFA code entry was canceled"); + await ShowLoginErrorAsync("MFA code entry was canceled"); + } + } + else + { + Log.Error("Login failed: {Error}", _client?.Network?.LoginMessage); + await ShowLoginErrorAsync($"Login failed: {_client?.Network?.LoginMessage}"); + } + } + + private async Task ProcessCapabilitiesAsync() + { + var capabilities = _client.Network.CurrentSim.Caps; + if (capabilities != null) + { + // TODO: Process the capabilities as needed + } + + await Task.CompletedTask; + } + + private void OnLoginProgress(object? sender, LoginProgressEventArgs e) + { + Log.Information("Login progress: {Status}", e.Status); + switch (e.Status) + { + case LoginStatus.ConnectingToLogin: + LoginStatusMessage = "Connecting to login server..."; + break; + case LoginStatus.ConnectingToSim: + LoginStatusMessage = "Connecting to region..."; + break; + case LoginStatus.Redirecting: + LoginStatusMessage = "Redirecting..."; + break; + case LoginStatus.ReadingResponse: + LoginStatusMessage = "Reading response..."; + break; + case LoginStatus.Success: + LoginStatusMessage = $"Logged in as {_client.Self.Name}"; + break; + case LoginStatus.Failed: + LoginStatusMessage = $"Login failed: {e.Message}"; + break; + case LoginStatus.None: + default: + LoginStatusMessage = $"Unknown login status: {e.Status}"; + break; + } + } + + private async Task ShowMfaInputDialogAsync() + { + // TODO: Implement MFA input dialog + return await Task.FromResult(string.Empty); + } } } \ No newline at end of file diff --git a/GalaxyViewer/ViewModels/MainViewModel.cs b/GalaxyViewer/ViewModels/MainViewModel.cs index 19766c5..96d0f98 100644 --- a/GalaxyViewer/ViewModels/MainViewModel.cs +++ b/GalaxyViewer/ViewModels/MainViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.IO; using System.Reactive; using System.Threading.Tasks; @@ -7,7 +6,6 @@ using System.Windows.Input; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; -using GalaxyViewer.Services; using GalaxyViewer.Views; using OpenMetaverse; using ReactiveUI; @@ -17,12 +15,12 @@ namespace GalaxyViewer.ViewModels { public class MainViewModel : ViewModelBase { + private UserControl? _currentView; private bool _isLoggedIn; private readonly GridClient _client = new(); private string? _username; - public string? Username { get => _username; @@ -30,7 +28,6 @@ namespace GalaxyViewer.ViewModels } private string? _password; - public string? Password { get => _password; @@ -38,7 +35,6 @@ namespace GalaxyViewer.ViewModels } private string? _loginLocation; - public string? LoginLocation { get => _loginLocation; @@ -46,44 +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 LogoutCommand { get; } public ReactiveCommand LoginCommand { get; } - public ICommand ShowPreferencesCommand { get; } - public ICommand ShowDevViewCommand { get; } - public ICommand ExitCommand { get; } - public MainViewModel(NavigationService navigationService) + public MainViewModel() { + _currentView = new LoginView(); IsLoggedIn = false; // By default you aren't logged in LogoutCommand = ReactiveCommand.Create(Logout); - LoginCommand = ReactiveCommand.Create(() => navigationService.Navigate("login")); - + LoginCommand = ReactiveCommand.CreateFromTask(Login); ShowPreferencesCommand = ReactiveCommand.Create(ShowPreferences); - ShowDevViewCommand = ReactiveCommand.Create(() => navigationService.Navigate("debug")); - ExitCommand = ReactiveCommand.Create(ExitApplication); - - // Navigate to login view on startup - if (!IsLoggedIn) - { - navigationService.Navigate("login"); - } } private void Logout() @@ -93,24 +87,22 @@ namespace GalaxyViewer.ViewModels IsLoggedIn = false; } - public async Task Login(string username, string password) + private async Task Login() { - /*try + try { - if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) + // Validate properties before using them + if (string.IsNullOrEmpty(Username) || string.IsNullOrEmpty(Password)) { - await File.AppendAllTextAsync("error.log", - "Invalid login parameters for user: " + username); - return; + // 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 libreMetaverseLoginParams = - _client.Network.DefaultLoginParams(username, password, userAgent, LoginLocation, - Grid); + var loginParams = _client.Network.DefaultLoginParams(Username, Password, userAgent, LoginLocation, Grid); - var loginSuccess = - await Task.Run(() => _client.Network.Login(libreMetaverseLoginParams)); + var loginSuccess = await Task.Run(() => _client.Network.Login(loginParams)); if (loginSuccess) { @@ -118,19 +110,21 @@ namespace GalaxyViewer.ViewModels } else { - Log.Error("Failed to login user: {Username}", username); + // Handle failed login + Log.Error("Failed to login user: {Username}", Username); } } catch (Exception ex) { - Log.Error(ex, "An error occurred while logging in user: {Username}", username); - }*/ + // Handle any exceptions that occur during login + Log.Error(ex, "An error occurred while logging in user: {Username}", Username); + } } - private static void ShowPreferences() + private void ShowPreferences() { #if ANDROID - _navigationService.Navigate("preferences"); + CurrentView = new PreferencesView(); #else var preferencesWindow = new PreferencesWindow { @@ -142,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(); } } } \ No newline at end of file diff --git a/GalaxyViewer/ViewModels/PreferencesViewModel.cs b/GalaxyViewer/ViewModels/PreferencesViewModel.cs index 16e8107..0e91df8 100644 --- a/GalaxyViewer/ViewModels/PreferencesViewModel.cs +++ b/GalaxyViewer/ViewModels/PreferencesViewModel.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Threading.Tasks; using System.Windows.Input; @@ -17,15 +18,24 @@ namespace GalaxyViewer.ViewModels { if (App.PreferencesManager != null) _preferencesManager = App.PreferencesManager; _preferences = App.PreferencesManager?.CurrentPreferences ?? new PreferencesModel(); - ThemeOptions = new ObservableCollection(_preferences.ThemeOptions); - LoginLocationOptions = new ObservableCollection(_preferences.LoginLocationOptions); - LanguageOptions = new ObservableCollection(_preferences.LanguageOptions); - FontOptions = new ObservableCollection(_preferences.FontOptions); + var preferencesOptions = _preferencesManager?.GetCurrentPreferencesOptions(); + ThemeOptions = new ObservableCollection(preferencesOptions?["ThemeOptions"] ?? + []); + LoginLocationOptions = new ObservableCollection( + preferencesOptions?["LoginLocationOptions"] ?? + []); + LanguageOptions = new ObservableCollection( + preferencesOptions?["LanguageOptions"] ?? + []); + FontOptions = + new ObservableCollection(preferencesOptions?["FontOptions"] ?? []); _selectedTheme = _preferences.Theme; _selectedLoginLocation = _preferences.LoginLocation; _selectedLanguage = _preferences.Language; _selectedFont = _preferences.Font; + _selectedGridNick = _preferences.SelectedGridNick; SaveCommand = new RelayCommand(async () => await SavePreferencesAsync()); + LoadPreferences(); } private async void LoadPreferences() @@ -34,20 +44,14 @@ 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; + SelectedTheme = _preferences.Theme; + SelectedLoginLocation = _preferences.LoginLocation; + SelectedLanguage = _preferences.Language; + SelectedFont = _preferences.Font; + SelectedGridNick = _preferences.SelectedGridNick; + var gridOptions = _preferencesManager?.GetGridOptions(); + GridOptions = new ObservableCollection(gridOptions ?? []); - OnPropertyChanged(nameof(SelectedTheme)); - OnPropertyChanged(nameof(SelectedLoginLocation)); - OnPropertyChanged(nameof(SelectedLanguage)); - OnPropertyChanged(nameof(SelectedFont)); _isLoadingPreferences = false; } @@ -55,22 +59,22 @@ namespace GalaxyViewer.ViewModels public ObservableCollection LoginLocationOptions { get; private set; } public ObservableCollection LanguageOptions { get; private set; } public ObservableCollection FontOptions { get; private set; } + public ObservableCollection GridOptions { get; private set; } private string _selectedLoginLocation; private string _selectedLanguage; private string _selectedFont; private string _selectedTheme; + private string _selectedGridNick; public string SelectedTheme { get => _selectedTheme; set { - if (_selectedTheme != value && !_isLoadingPreferences) - { - _selectedTheme = value; - OnPropertyChanged(nameof(SelectedTheme)); - } + if (_selectedTheme == value || _isLoadingPreferences) return; + _selectedTheme = value; + OnPropertyChanged(nameof(SelectedTheme)); } } @@ -79,11 +83,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 +94,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,11 +105,20 @@ 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)); + } + } + + public string SelectedGridNick + { + get => _selectedGridNick; + set + { + if (_selectedGridNick == value || _isLoadingPreferences) return; + _selectedGridNick = value; + OnPropertyChanged(nameof(SelectedGridNick)); } } @@ -119,6 +128,7 @@ namespace GalaxyViewer.ViewModels _preferences.LoginLocation = SelectedLoginLocation; _preferences.Language = SelectedLanguage; _preferences.Font = SelectedFont; + _preferences.SelectedGridNick = SelectedGridNick; if (_preferencesManager != null) await _preferencesManager.SavePreferencesAsync(_preferences); diff --git a/GalaxyViewer/ViewModels/SetProperty.cs b/GalaxyViewer/ViewModels/SetProperty.cs index 1b2c418..8053084 100644 --- a/GalaxyViewer/ViewModels/SetProperty.cs +++ b/GalaxyViewer/ViewModels/SetProperty.cs @@ -1,23 +1,13 @@ using System.ComponentModel; -using System.Runtime.CompilerServices; -namespace GalaxyViewer.ViewModels +namespace GalaxyViewer.ViewModels; + +public abstract partial class ViewModelBase : INotifyPropertyChanged { - public abstract partial class ViewModelBase : INotifyPropertyChanged + public new event PropertyChangedEventHandler? PropertyChanged; + + protected void OnPropertyChanged(string propertyName) { - public new event PropertyChangedEventHandler? PropertyChanged; - - protected bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = null!) - { - if (Equals(field, value)) return false; - field = value; - OnPropertyChanged(propertyName); - return true; - } - - protected void OnPropertyChanged(string propertyName) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } \ No newline at end of file diff --git a/GalaxyViewer/Views/BaseWindow.cs b/GalaxyViewer/Views/BaseWindow.cs index a6fa4bc..6370d0e 100644 --- a/GalaxyViewer/Views/BaseWindow.cs +++ b/GalaxyViewer/Views/BaseWindow.cs @@ -1,25 +1,53 @@ -using Avalonia.Controls; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Media; using Avalonia.Styling; using GalaxyViewer.Models; +using Ursa.ReactiveUIExtension; -namespace GalaxyViewer.Views +namespace GalaxyViewer.Views; + +public class BaseWindow : ReactiveUrsaWindow, IStyleable { - public class BaseWindow : Window - { - protected BaseWindow() - { - Icon = new WindowIcon("Assets/galaxy.ico"); - CanResize = true; - App.PreferencesManager!.PreferencesChanged += OnPreferencesChanged; - ApplyTheme(App.PreferencesManager.LoadPreferencesAsync().Result.Theme); - } + Type IStyleable.StyleKey => typeof(Window); - private void OnPreferencesChanged(object? sender, PreferencesModel preferences) + protected BaseWindow() + { + Title = "GalaxyViewer"; + Icon = new WindowIcon("Assets/GalaxyViewerLogo.ico"); + CanResize = true; + 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); + }); + } - public void ApplyTheme(string theme) + private void OnPreferencesChanged(object? sender, PreferencesModel preferences) + { + Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => + { + ApplyTheme(preferences.Theme); + FontFamily = new FontFamily(preferences.Font); + }); + } + + internal void ApplyTheme(string theme) + { + Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { RequestedThemeVariant = theme switch { @@ -27,6 +55,6 @@ namespace GalaxyViewer.Views "Dark" => ThemeVariant.Dark, _ => ThemeVariant.Default }; - } + }); } } \ No newline at end of file diff --git a/GalaxyViewer/Views/DebugView.axaml.cs b/GalaxyViewer/Views/DebugView.axaml.cs index f2d093f..1f6385c 100644 --- a/GalaxyViewer/Views/DebugView.axaml.cs +++ b/GalaxyViewer/Views/DebugView.axaml.cs @@ -1,18 +1,17 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace GalaxyViewer.Views -{ - public partial class DebugView : UserControl - { - public DebugView() - { - InitializeComponent(); - } +namespace GalaxyViewer.Views; - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } +public partial class DebugView : UserControl +{ + public DebugView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); } } \ No newline at end of file diff --git a/GalaxyViewer/Views/LoggedInView.axaml.cs b/GalaxyViewer/Views/LoggedInView.axaml.cs index 887e771..06541c1 100644 --- a/GalaxyViewer/Views/LoggedInView.axaml.cs +++ b/GalaxyViewer/Views/LoggedInView.axaml.cs @@ -1,18 +1,17 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace GalaxyViewer.Views -{ - public partial class LoggedInView : UserControl - { - public LoggedInView() - { - InitializeComponent(); - } +namespace GalaxyViewer.Views; - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } +public partial class LoggedInView : UserControl +{ + public LoggedInView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); } } \ No newline at end of file diff --git a/GalaxyViewer/Views/LoginView.axaml b/GalaxyViewer/Views/LoginView.axaml index b603aa3..39d4bc0 100644 --- a/GalaxyViewer/Views/LoginView.axaml +++ b/GalaxyViewer/Views/LoginView.axaml @@ -1,45 +1,49 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + +