diff --git a/.gitignore b/.gitignore
index 8afdcb6..0ec3d0d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -452,3 +452,21 @@ $RECYCLE.BIN/
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
+
+##
+## GalaxyViewer specific build things
+##
+
+# AppImage files
+*.AppImage
+
+# Build output directory (already covered by publish/ above)
+/publish/
+
+# Desktop files generated during build
+*.desktop
+
+# LinuxDeploy build artifacts
+linuxdeploy-output/
+linuxdeploy-*.AppImage
+appimagetool-*.AppImage
diff --git a/Directory.Build.props b/Directory.Build.props
index 37fd070..ee78009 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,10 +1,8 @@
- 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 59e4709..02867a9 100644
--- a/GalaxyViewer.Android/GalaxyViewer.Android.csproj
+++ b/GalaxyViewer.Android/GalaxyViewer.Android.csproj
@@ -15,8 +15,8 @@
-
-
+
+
diff --git a/GalaxyViewer.Android/MainActivity.cs b/GalaxyViewer.Android/MainActivity.cs
index 01e23ec..870f731 100644
--- a/GalaxyViewer.Android/MainActivity.cs
+++ b/GalaxyViewer.Android/MainActivity.cs
@@ -1,14 +1,9 @@
-using Android;
using Android.App;
using Android.Content.PM;
-using Android.OS;
-using Android.Widget;
-using AndroidX.Core.App;
-using AndroidX.Core.Content;
+using Android.Views;
using Avalonia;
using Avalonia.Android;
using Avalonia.ReactiveUI;
-using GalaxyViewer.Services;
namespace GalaxyViewer.Android
{
@@ -17,60 +12,11 @@ namespace GalaxyViewer.Android
Theme = "@style/MyTheme.NoActionBar",
Icon = "@drawable/icon",
MainLauncher = true,
- ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
+ WindowSoftInputMode = SoftInput.AdjustResize,
+ ConfigurationChanges =
+ ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
public class MainActivity : AvaloniaMainActivity
{
- 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
- {
- LoadResources();
- // TODO: Fix this so that it doesn't instantly crash and asks for permission before loading resources
- }
- }
-
- 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 loading resources
- LoadResources();
- }
- else
- {
- // Permission denied, show a message to the user
- var message = "Permission to access storage denied. The application will not be able to load resources.";
- var toast = Toast.MakeText(this, message, ToastLength.Long);
- toast.Show();
- }
- }
- }
-
- private void LoadResources()
- {
- // Load your resources here
- InitializeLiteDbService();
- }
-
- private void InitializeLiteDbService()
- {
- // Initialize LiteDbService here
- var liteDbService = new LiteDbService();
- // Use liteDbService as needed
- }
-
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return base.CustomizeAppBuilder(builder)
diff --git a/GalaxyViewer.Android/Properties/AndroidManifest.xml b/GalaxyViewer.Android/Properties/AndroidManifest.xml
index bc9a332..c265097 100644
--- a/GalaxyViewer.Android/Properties/AndroidManifest.xml
+++ b/GalaxyViewer.Android/Properties/AndroidManifest.xml
@@ -1,14 +1,38 @@
-
+
+
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/GalaxyViewer.Desktop/GalaxyViewer.Desktop.csproj b/GalaxyViewer.Desktop/GalaxyViewer.Desktop.csproj
index 194d330..98795d8 100644
--- a/GalaxyViewer.Desktop/GalaxyViewer.Desktop.csproj
+++ b/GalaxyViewer.Desktop/GalaxyViewer.Desktop.csproj
@@ -13,9 +13,9 @@
-
+
-
+
diff --git a/GalaxyViewer.Desktop/Program.cs b/GalaxyViewer.Desktop/Program.cs
index 49004d9..b5f5b51 100644
--- a/GalaxyViewer.Desktop/Program.cs
+++ b/GalaxyViewer.Desktop/Program.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Avalonia;
using Avalonia.ReactiveUI;
@@ -20,4 +20,4 @@ sealed class Program
.WithInterFont()
.LogToTrace()
.UseReactiveUI();
-}
+}
\ No newline at end of file
diff --git a/GalaxyViewer/App.axaml b/GalaxyViewer/App.axaml
index fb4985b..866e191 100644
--- a/GalaxyViewer/App.axaml
+++ b/GalaxyViewer/App.axaml
@@ -10,6 +10,7 @@
+
@@ -18,8 +19,15 @@
+
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/GalaxyViewer/App.axaml.cs b/GalaxyViewer/App.axaml.cs
index 55b86d7..f023d90 100644
--- a/GalaxyViewer/App.axaml.cs
+++ b/GalaxyViewer/App.axaml.cs
@@ -1,19 +1,21 @@
using System;
using System.ComponentModel;
-using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Avalonia;
-using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
+using Avalonia.Markup.Xaml.Styling;
using Avalonia.Styling;
+using Avalonia.Platform;
+using Avalonia.Media;
using GalaxyViewer.Models;
using GalaxyViewer.Services;
using GalaxyViewer.ViewModels;
using GalaxyViewer.Views;
using Microsoft.Extensions.DependencyInjection;
+using OpenMetaverse;
using Serilog;
namespace GalaxyViewer;
@@ -22,8 +24,24 @@ public class App : Application, IDisposable
{
private static IServiceProvider? _serviceProvider;
public static PreferencesManager? PreferencesManager { get; private set; }
- private static LiteDbService _liteDbService;
- private static SessionModel _session;
+ private static LiteDbService? _liteDbService;
+ private static GridClient? _gridClient;
+
+ private static bool _isLoggedIn;
+ private IPlatformSettings? _platformSettings;
+
+ public static event PropertyChangedEventHandler? StaticPropertyChanged;
+
+ public static bool IsLoggedIn
+ {
+ get => _isLoggedIn;
+ set
+ {
+ if (_isLoggedIn == value) return;
+ _isLoggedIn = value;
+ OnStaticPropertyChanged();
+ }
+ }
public App()
{
@@ -37,15 +55,27 @@ public class App : Application, IDisposable
"GalaxyViewer", "logs", "error.log");
Log.Logger = new LoggerConfiguration()
- .WriteTo.Console()
- .WriteTo.File(logFilePath, rollingInterval: RollingInterval.Day)
+ .MinimumLevel.Information()
+ .WriteTo.Console(
+ outputTemplate:
+ "[{Timestamp:HH:mm:ss} {Level:u3}] GALAXYVIEWER: {Message:lj}{NewLine}{Exception}")
+ .WriteTo.File(logFilePath,
+ rollingInterval: RollingInterval.Day,
+ outputTemplate:
+ "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
}
private static void ConfigureServices(IServiceCollection services)
{
- services.AddSingleton();
- // Register other services here
+ _gridClient = new GridClient();
+ services.AddSingleton(_gridClient);
+ services.AddSingleton(_ => new LiteDbService(_gridClient));
+ services.AddSingleton(provider =>
+ new SessionService(
+ provider.GetRequiredService(),
+ provider.GetRequiredService()
+ ));
}
public override void Initialize()
@@ -54,74 +84,124 @@ public class App : Application, IDisposable
ConfigureServices(serviceCollection);
_serviceProvider = serviceCollection.BuildServiceProvider();
- _liteDbService = _serviceProvider.GetService();
- if (_liteDbService == null)
- {
- throw new InvalidOperationException("LiteDbService is not registered.");
- }
+ _liteDbService = _serviceProvider.GetRequiredService();
PreferencesManager = new PreferencesManager(_liteDbService);
+
PreferencesManager.PreferencesChanged += OnPreferencesChanged;
- _session = _liteDbService.GetSession();
-
AvaloniaXamlLoader.Load(this);
- base.Initialize();
- }
- public static bool IsLoggedIn
- {
- get => _session.IsLoggedIn;
- set
+ try
{
- if (_session.IsLoggedIn != value)
+ _platformSettings = PlatformSettings;
+ if (_platformSettings != null)
{
- _session.IsLoggedIn = value;
- _liteDbService.SaveSession(_session);
- OnStaticPropertyChanged();
+ _platformSettings.ColorValuesChanged += OnSystemThemeChanged;
}
}
+ catch (Exception ex)
+ {
+ Log.Warning(ex, "Failed to subscribe to system theme changes");
+ }
+
+ base.Initialize();
}
public override void OnFrameworkInitializationCompleted()
{
try
{
+ if (_serviceProvider == null)
+ {
+ Log.Error("Service provider is null during framework initialization");
+ throw new InvalidOperationException(
+ "Service provider was not properly initialized");
+ }
+
+ var liteDbService = _serviceProvider.GetRequiredService();
+ var gridClient = _serviceProvider.GetRequiredService();
+ var sessionService = _serviceProvider.GetRequiredService();
+
+ Log.Information("Services initialized successfully");
+
+ // Create UI first to avoid blocking
switch (ApplicationLifetime)
{
case IClassicDesktopStyleApplicationLifetime desktop:
- Log.Information("Initializing MainWindow for desktop application.");
desktop.MainWindow = new MainWindow
{
- DataContext = new MainViewModel(_liteDbService)
+ DataContext = new MainViewModel(liteDbService, gridClient, sessionService)
};
desktop.MainWindow.Show();
+ Log.Information("Desktop main window created and shown");
break;
case ISingleViewApplicationLifetime singleViewPlatform:
- Log.Information("Initializing MainView for single view application.");
- singleViewPlatform.MainView = new MainView
- {
- DataContext = new MainViewModel(_liteDbService)
- };
+ Log.Information("Creating MainView for single view platform (Android)");
+ break;
+ default:
+ Log.Warning("Unknown application lifetime type: {Type}",
+ ApplicationLifetime?.GetType().Name);
break;
}
+
+ Task.Run(async () =>
+ {
+ try
+ {
+ if (PreferencesManager != null)
+ {
+ var preferences = await PreferencesManager.LoadPreferencesAsync();
+ if (preferences != null)
+ {
+ ApplyPreferences(preferences);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Warning(ex, "Failed to load initial preferences");
+ }
+ });
}
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
+ throw;
}
base.OnFrameworkInitializationCompleted();
}
- public static event PropertyChangedEventHandler? StaticPropertyChanged;
- private static void OnStaticPropertyChanged([CallerMemberName] string propertyName = null)
+ private static void OnStaticPropertyChanged([CallerMemberName] string? propertyName = null)
{
StaticPropertyChanged?.Invoke(null, new PropertyChangedEventArgs(propertyName));
}
+ private void OnSystemThemeChanged(object? sender, PlatformColorValues e)
+ {
+ Task.Run(async () =>
+ {
+ try
+ {
+ if (PreferencesManager != null)
+ {
+ var preferences = await PreferencesManager.LoadPreferencesAsync();
+ if (preferences?.Theme == "System")
+ {
+ ApplyPreferences(preferences);
+ RefreshThemeForAllWindows();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Warning(ex, "Error handling system theme change");
+ }
+ });
+ }
+
private void OnPreferencesChanged(object? sender, PreferencesModel preferences)
{
ApplyPreferences(preferences);
@@ -132,36 +212,192 @@ public class App : Application, IDisposable
{
Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
{
- RequestedThemeVariant = preferences.Theme switch
- {
- "Light" => ThemeVariant.Light,
- "Dark" => ThemeVariant.Dark,
- _ => ThemeVariant.Default
- };
+ RequestedThemeVariant = GetThemeVariant(preferences.Theme);
- // TODO: Apply other preferences
+ var isDarkTheme = RequestedThemeVariant == ThemeVariant.Dark ||
+ (RequestedThemeVariant == ThemeVariant.Default &&
+ DetectSystemTheme() == ThemeVariant.Dark);
+
+ UpdateAccentColorResource(preferences.AccentColor);
+ UpdateTextColorResource(isDarkTheme);
});
}
+ private void UpdateAccentColorResource(string accentColorPreference)
+ {
+ try
+ {
+ IBrush accentBrush;
+
+ if (accentColorPreference == "System Default")
+ {
+ accentBrush = GetSystemAccentBrush();
+ }
+ else
+ {
+ var isDarkTheme = RequestedThemeVariant == ThemeVariant.Dark ||
+ (RequestedThemeVariant == ThemeVariant.Default &&
+ DetectSystemTheme() == ThemeVariant.Dark);
+
+ var colorValue =
+ PreferencesOptions.GetAccentColorForTheme(accentColorPreference, isDarkTheme);
+
+ try
+ {
+ accentBrush = new SolidColorBrush(Color.Parse(colorValue));
+ }
+ catch
+ {
+ accentBrush = GetSystemAccentBrush();
+ }
+ }
+
+ Resources["SystemAccentColorBrush"] = accentBrush;
+ Resources["SystemAccentColor"] = ((SolidColorBrush)accentBrush).Color;
+ // Ensure these resources are available for DynamicResource lookups
+ if (Current == null) return;
+ Current.Resources["SystemAccentColorBrush"] = accentBrush;
+ Current.Resources["SystemAccentColor"] = ((SolidColorBrush)accentBrush).Color;
+ }
+ catch (Exception ex)
+ {
+ Log.Warning(ex, "Failed to update accent color resource");
+ }
+ }
+
+ internal static IBrush GetSystemAccentBrush()
+ {
+ try
+ {
+ if (Current?.TryGetResource("SystemAccentColorBrush",
+ Current.ActualThemeVariant, out var resource) == true)
+ {
+ if (resource is IBrush brush)
+ return brush;
+ }
+
+ if (Current?.TryGetResource("SystemAccentColor",
+ Current.ActualThemeVariant, out var colorResource) == true)
+ {
+ if (colorResource is Color color)
+ return new SolidColorBrush(color);
+ }
+ }
+ catch
+ {
+ // Continue to fallback
+ }
+
+ // Final fallback to a reasonable blue
+ return new SolidColorBrush(Color.Parse("#0078D4"));
+ }
+
+ private void UpdateTextColorResource(bool isDarkTheme)
+ {
+ var color = isDarkTheme ? Color.Parse("#F0F0F0") : Color.Parse("#222222");
+ var brush = new SolidColorBrush(color);
+
+ Resources["TextColor"] = color;
+ Resources["TextColorBrush"] = brush;
+ if (Current != null)
+ {
+ Current.Resources["TextColor"] = color;
+ Current.Resources["TextColorBrush"] = brush;
+ }
+ }
+
+ private ThemeVariant GetThemeVariant(string themePreference)
+ {
+ return themePreference switch
+ {
+ "Light" => ThemeVariant.Light,
+ "Dark" => ThemeVariant.Dark,
+ "System" => DetectSystemTheme(),
+ _ => ThemeVariant.Default
+ };
+ }
+
+ private ThemeVariant DetectSystemTheme()
+ {
+ try
+ {
+ if (_platformSettings != null)
+ {
+ var colorValues = _platformSettings.GetColorValues();
+ return colorValues.ThemeVariant == PlatformThemeVariant.Dark
+ ? ThemeVariant.Dark
+ : ThemeVariant.Light;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Warning(ex, "Failed to detect system theme, falling back to default");
+ }
+
+ return ThemeVariant.Default;
+ }
+
private async void RefreshThemeForAllWindows()
{
- if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktopLifetime)
- return;
-
- await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(async () =>
+ try
{
- foreach (var window in desktopLifetime.Windows)
+ if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktopLifetime)
+ return;
+
+ await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(async () =>
{
- if (window is not BaseWindow baseWindow) continue;
- var resultTheme = (await PreferencesManager?.LoadPreferencesAsync())?.Theme;
- if (resultTheme != null)
- baseWindow.ApplyTheme(resultTheme);
- }
- });
+ try
+ {
+ if (PreferencesManager != null)
+ {
+ var preferences = await PreferencesManager.LoadPreferencesAsync();
+ if (preferences?.Theme != null)
+ {
+ foreach (var window in desktopLifetime.Windows)
+ {
+ if (window is BaseWindow baseWindow)
+ {
+ baseWindow.ApplyTheme(preferences.Theme);
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Warning(ex, "Error refreshing themes for windows");
+ }
+ });
+ }
+ catch (Exception e)
+ {
+ Log.Warning(e, "Failed to refresh theme for all windows");
+ }
+ }
+
+ // TODO: Implement a method to set language resources based on user preferences
+ // Currently we only have US English resources
+ private static void SetLanguageResources()
+ {
+ var language = PreferencesManager?.CurrentPreferences?.Language ?? "en-US";
+ var resources = Current?.Resources;
+ if (resources == null) return;
+ resources.MergedDictionaries.Clear();
+
+ if (language == "en-US")
+ {
+ resources.MergedDictionaries.Add(
+ new ResourceInclude(new Uri("avares://GalaxyViewer/Resources/Strings.axaml")));
+ }
}
public void Dispose()
{
+ if (_platformSettings != null)
+ {
+ _platformSettings.ColorValuesChanged -= OnSystemThemeChanged;
+ }
+
//PreferencesManager?.Dispose();
(_serviceProvider as IDisposable)?.Dispose();
}
diff --git a/GalaxyViewer/Assets/Fonts/NotoColorEmoji.ttf b/GalaxyViewer/Assets/Fonts/NotoColorEmoji.ttf
index cf7a47e..5fb1151 100644
Binary files a/GalaxyViewer/Assets/Fonts/NotoColorEmoji.ttf and b/GalaxyViewer/Assets/Fonts/NotoColorEmoji.ttf differ
diff --git a/GalaxyViewer/Assets/GalaxyViewerLogo-0.png b/GalaxyViewer/Assets/GalaxyViewerLogo-0.png
new file mode 100644
index 0000000..0e4dc29
Binary files /dev/null and b/GalaxyViewer/Assets/GalaxyViewerLogo-0.png differ
diff --git a/GalaxyViewer/Assets/GalaxyViewerLogo-1.png b/GalaxyViewer/Assets/GalaxyViewerLogo-1.png
new file mode 100644
index 0000000..95e706b
Binary files /dev/null and b/GalaxyViewer/Assets/GalaxyViewerLogo-1.png differ
diff --git a/GalaxyViewer/Assets/GalaxyViewerLogo-2.png b/GalaxyViewer/Assets/GalaxyViewerLogo-2.png
new file mode 100644
index 0000000..d7a209a
Binary files /dev/null and b/GalaxyViewer/Assets/GalaxyViewerLogo-2.png differ
diff --git a/GalaxyViewer/Assets/Localization/LocalizationManager.cs b/GalaxyViewer/Assets/Localization/LocalizationManager.cs
deleted file mode 100644
index e38578e..0000000
--- a/GalaxyViewer/Assets/Localization/LocalizationManager.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using System.ComponentModel;
-using System.Globalization;
-using System.Resources;
-
-namespace GalaxyViewer.Assets.Localization;
-
-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)
- {
- 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));
- }
- }
-}
\ No newline at end of file
diff --git a/GalaxyViewer/Assets/Localization/Strings.Designer.cs b/GalaxyViewer/Assets/Localization/Strings.Designer.cs
deleted file mode 100644
index 8fc67ae..0000000
--- a/GalaxyViewer/Assets/Localization/Strings.Designer.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-//------------------------------------------------------------------------------
-//
-// This code was generated by a tool.
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-//
-//------------------------------------------------------------------------------
-
-namespace GalaxyViewer.Assets.Localization {
- using System;
-
-
- ///
- /// A strongly-typed resource class, for looking up localized strings, etc.
- ///
- // This class was auto-generated by the StronglyTypedResourceBuilder
- // class via a tool like ResGen or Visual Studio.
- // To add or remove a member, edit your .ResX file then rerun ResGen
- // with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- internal class Strings {
-
- private static global::System.Resources.ResourceManager resourceMan;
-
- private static global::System.Globalization.CultureInfo resourceCulture;
-
- [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
- internal Strings() {
- }
-
- ///
- /// Returns the cached ResourceManager instance used by this class.
- ///
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Resources.ResourceManager ResourceManager {
- get {
- if (object.ReferenceEquals(resourceMan, null)) {
- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GalaxyViewer.Assets.Localization.Strings", typeof(Strings).Assembly);
- resourceMan = temp;
- }
- return resourceMan;
- }
- }
-
- ///
- /// Overrides the current thread's CurrentUICulture property for all
- /// resource lookups using this strongly typed resource class.
- ///
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Globalization.CultureInfo Culture {
- get {
- return resourceCulture;
- }
- set {
- resourceCulture = value;
- }
- }
- }
-}
diff --git a/GalaxyViewer/Assets/Localization/Strings.en-US.Designer.cs b/GalaxyViewer/Assets/Localization/Strings.en-US.Designer.cs
deleted file mode 100644
index c6df0a0..0000000
--- a/GalaxyViewer/Assets/Localization/Strings.en-US.Designer.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-//------------------------------------------------------------------------------
-//
-// This code was generated by a tool.
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-//
-//------------------------------------------------------------------------------
-
-namespace GalaxyViewer.Assets.Localization {
- using System;
-
-
- ///
- /// A strongly-typed resource class, for looking up localized strings, etc.
- ///
- // This class was auto-generated by the StronglyTypedResourceBuilder
- // class via a tool like ResGen or Visual Studio.
- // To add or remove a member, edit your .ResX file then rerun ResGen
- // with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- internal class Strings_en_US {
-
- private static global::System.Resources.ResourceManager resourceMan;
-
- private static global::System.Globalization.CultureInfo resourceCulture;
-
- [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
- internal Strings_en_US() {
- }
-
- ///
- /// Returns the cached ResourceManager instance used by this class.
- ///
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Resources.ResourceManager ResourceManager {
- get {
- if (object.ReferenceEquals(resourceMan, null)) {
- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GalaxyViewer.Assets.Localization.Strings.en-US", typeof(Strings_en_US).Assembly);
- resourceMan = temp;
- }
- return resourceMan;
- }
- }
-
- ///
- /// Overrides the current thread's CurrentUICulture property for all
- /// resource lookups using this strongly typed resource class.
- ///
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Globalization.CultureInfo Culture {
- get {
- return resourceCulture;
- }
- set {
- resourceCulture = value;
- }
- }
-
- ///
- /// Looks up a localized string similar to Login.
- ///
- internal static string LoginScreenLoginButton {
- get {
- return ResourceManager.GetString("LoginScreenLoginButton", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Password.
- ///
- internal static string LoginScreenPassword {
- get {
- return ResourceManager.GetString("LoginScreenPassword", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Register.
- ///
- internal static string LoginScreenRegisterButton {
- get {
- return ResourceManager.GetString("LoginScreenRegisterButton", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Login.
- ///
- internal static string LoginScreenTitle {
- get {
- return ResourceManager.GetString("LoginScreenTitle", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Username.
- ///
- internal static string LoginScreenUsername {
- get {
- return ResourceManager.GetString("LoginScreenUsername", resourceCulture);
- }
- }
- }
-}
diff --git a/GalaxyViewer/Assets/Localization/Strings.en-US.resx b/GalaxyViewer/Assets/Localization/Strings.en-US.resx
deleted file mode 100644
index 3d14c34..0000000
--- a/GalaxyViewer/Assets/Localization/Strings.en-US.resx
+++ /dev/null
@@ -1,84 +0,0 @@
-
-
-
-
-
-
-
-
-
- text/microsoft-resx
-
-
- 1.3
-
-
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
- PublicKeyToken=b77a5c561934e089
-
-
-
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
- PublicKeyToken=b77a5c561934e089
-
-
-
-
-
- Welcome to GalaxyViewer 🌌
-
-
- Error
-
-
- Ok
-
-
- Cancel
-
-
-
- Login
-
-
- Username
-
-
- Password
-
-
- Login Location
-
-
- Grid
-
-
- Login
-
-
- Login successful
-
-
- Wrong username or password
-
-
-
- Preferences
-
-
- Language
-
-
- Login Location
-
-
- Theme
-
-
- Font
-
-
- Save Preferences
-
-
\ No newline at end of file
diff --git a/GalaxyViewer/Assets/Localization/Strings.resx b/GalaxyViewer/Assets/Localization/Strings.resx
deleted file mode 100644
index 54388f7..0000000
--- a/GalaxyViewer/Assets/Localization/Strings.resx
+++ /dev/null
@@ -1,82 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- text/microsoft-resx
-
-
- 1.3
-
-
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
-
- Welcome to GalaxyViewer 🌌
-
-
- Error
-
-
- Ok
-
-
- Cancel
-
-
-
- Login
-
-
- Username
-
-
- Password
-
-
- Login Location
-
-
- Grid
-
-
- Login
-
-
- Login successful
-
-
- Wrong username or password
-
-
-
- Preferences
-
-
- Language
-
-
- Login Location
-
-
- Theme
-
-
- Font
-
-
- Save Preferences
-
-
\ No newline at end of file
diff --git a/GalaxyViewer/Commands/RelayCommand.cs b/GalaxyViewer/Commands/RelayCommand.cs
index 01e1fdc..f1d6a98 100644
--- a/GalaxyViewer/Commands/RelayCommand.cs
+++ b/GalaxyViewer/Commands/RelayCommand.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Threading.Tasks;
using System.Windows.Input;
diff --git a/GalaxyViewer/Controls/AddressBar.axaml b/GalaxyViewer/Controls/AddressBar.axaml
new file mode 100644
index 0000000..0bb7ef9
--- /dev/null
+++ b/GalaxyViewer/Controls/AddressBar.axaml
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/GalaxyViewer/Controls/AddressBar.axaml.cs b/GalaxyViewer/Controls/AddressBar.axaml.cs
new file mode 100644
index 0000000..9b98102
--- /dev/null
+++ b/GalaxyViewer/Controls/AddressBar.axaml.cs
@@ -0,0 +1,43 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using System.Linq;
+using Avalonia.VisualTree;
+using GalaxyViewer.ViewModels;
+using System.Reactive;
+using Avalonia.Markup.Xaml;
+
+namespace GalaxyViewer.Controls
+{
+ public partial class AddressBar : UserControl
+ {
+ public AddressBar()
+ {
+ InitializeComponent();
+ AddHandler(PointerPressedEvent, OnRootPointerPressed, handledEventsToo: true);
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void OnRootPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (DataContext is not AddressBarViewModel vm)
+ return;
+ if (!vm.IsEditing)
+ return;
+
+ var textBox = this.GetVisualDescendants().OfType().FirstOrDefault(tb => tb.IsVisible);
+ if (textBox == null)
+ return;
+
+ var pointerPos = e.GetPosition(textBox);
+ var bounds = new Avalonia.Rect(0, 0, textBox.Bounds.Width, textBox.Bounds.Height);
+ if (!bounds.Contains(pointerPos))
+ {
+ vm.CancelEditCommand.Execute(Unit.Default);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/GalaxyViewer/Controls/EmojiAwareTextBlock.cs b/GalaxyViewer/Controls/EmojiAwareTextBlock.cs
new file mode 100644
index 0000000..026f850
--- /dev/null
+++ b/GalaxyViewer/Controls/EmojiAwareTextBlock.cs
@@ -0,0 +1,73 @@
+using System.Collections.Generic;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Documents;
+using Avalonia.Media;
+
+namespace GalaxyViewer.Controls;
+
+public class EmojiAwareTextBlock : TextBlock
+{
+ public static readonly StyledProperty EmojiFontFamilyProperty =
+ AvaloniaProperty.Register(
+ nameof(EmojiFontFamily));
+
+ public FontFamily EmojiFontFamily
+ {
+ get => GetValue(EmojiFontFamilyProperty);
+ set => SetValue(EmojiFontFamilyProperty, value);
+ }
+
+ public EmojiAwareTextBlock()
+ {
+ PropertyChanged += (_, e) =>
+ {
+ if (e.Property == TextProperty || e.Property == EmojiFontFamilyProperty)
+ {
+ ApplyFonts();
+ }
+ };
+ }
+
+ private void ApplyFonts()
+ {
+ if (string.IsNullOrEmpty(Text))
+ return;
+
+ var inlines = new List();
+ for (var i = 0; i < Text.Length; i++)
+ {
+ string charOrEmoji;
+ bool isEmoji;
+
+ if (char.IsSurrogatePair(Text, i))
+ {
+ charOrEmoji = Text.Substring(i, 2);
+ isEmoji = true;
+ i++;
+ }
+ else
+ {
+ charOrEmoji = Text[i].ToString();
+ // This really could use some improvement, but it's as good as I can get for now
+ isEmoji = char.IsSurrogate(Text[i]) ||
+ (Text[i] >= '\u2600' && Text[i] <= '\u27BF') ||
+ (Text[i] >= '\u2B50' && Text[i] <= '\u2B55');
+ }
+
+ var run = new Run(charOrEmoji)
+ {
+ FontFamily = isEmoji ? EmojiFontFamily : FontFamily
+ };
+ inlines.Add(run);
+ }
+
+ if (Inlines == null)
+ {
+ Inlines = new InlineCollection();
+ }
+
+ Inlines.Clear();
+ Inlines.AddRange(inlines);
+ }
+}
\ No newline at end of file
diff --git a/GalaxyViewer/Controls/EmojiTextBlock.cs b/GalaxyViewer/Controls/EmojiTextBlock.cs
index e6820c8..5067c3f 100644
--- a/GalaxyViewer/Controls/EmojiTextBlock.cs
+++ b/GalaxyViewer/Controls/EmojiTextBlock.cs
@@ -1,28 +1,26 @@
-using System.Linq;
-using System.Text.RegularExpressions;
+using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Media;
+using Serilog;
namespace GalaxyViewer.Controls;
-public abstract partial class EmojiTextBlock : TextBlock
+public partial class EmojiTextBlock : TextBlock
{
- private static readonly Regex EmojiRegex = MyRegex();
-
public static readonly StyledProperty EmojiFontFamilyProperty =
AvaloniaProperty.Register(nameof(EmojiFontFamily));
+ public static readonly StyledProperty DefaultFontFamilyProperty =
+ AvaloniaProperty.Register(nameof(DefaultFontFamily));
+
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);
@@ -31,9 +29,11 @@ public abstract partial class EmojiTextBlock : TextBlock
protected EmojiTextBlock()
{
- this.PropertyChanged += (_, e) =>
+ PropertyChanged += (_, e) =>
{
- if (e.Property == TextProperty)
+ if (e.Property == TextProperty ||
+ e.Property == EmojiFontFamilyProperty ||
+ e.Property == DefaultFontFamilyProperty)
{
ApplyFonts();
}
@@ -45,12 +45,43 @@ public abstract partial class EmojiTextBlock : TextBlock
if (string.IsNullOrEmpty(Text))
return;
- var inlines = Text.Select(ch => new Run(ch.ToString()) { FontFamily = EmojiRegex.IsMatch(ch.ToString()) ? EmojiFontFamily : DefaultFontFamily }).Cast().ToList();
+ var inlines = new List();
+ for (int i = 0; i < Text.Length; i++)
+ {
+ string charOrEmoji;
+ bool isEmoji;
- Inlines?.Clear();
- Inlines?.AddRange(inlines);
+ if (char.IsSurrogatePair(Text, i))
+ {
+ charOrEmoji = Text.Substring(i, 2);
+ isEmoji = true;
+ i++;
+ }
+ else
+ {
+ charOrEmoji = Text[i].ToString();
+ isEmoji = char.IsSurrogate(Text[i]) ||
+ (Text[i] >= '\u2600' && Text[i] <= '\u27BF') ||
+ (Text[i] >= '\u2B50' && Text[i] <= '\u2B55');
+ }
+
+ var fontFamily = isEmoji ? EmojiFontFamily : DefaultFontFamily;
+
+ var run = new Run(charOrEmoji)
+ {
+ FontFamily = fontFamily
+ };
+ inlines.Add(run);
+ }
+
+ if (Inlines == null)
+ {
+ Inlines = new InlineCollection();
+ }
+
+ Inlines.Clear();
+ Inlines.AddRange(inlines);
+
+ Log.Information("Font application complete. Total inlines: {Count}", inlines.Count);
}
-
- [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/Converters/AccentColorConverter.cs b/GalaxyViewer/Converters/AccentColorConverter.cs
new file mode 100644
index 0000000..07ae376
--- /dev/null
+++ b/GalaxyViewer/Converters/AccentColorConverter.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using Avalonia.Styling;
+using GalaxyViewer.Models;
+
+namespace GalaxyViewer.Converters;
+
+public class AccentColorConverter : IValueConverter
+{
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is not string accentColorPreference || accentColorPreference == "System Default")
+ return GetSystemAccentBrush();
+
+ var isDarkTheme = IsCurrentThemeDark();
+
+ var colorValue = PreferencesOptions.GetAccentColorForTheme(accentColorPreference, isDarkTheme);
+
+ try
+ {
+ return new SolidColorBrush(Color.Parse(colorValue));
+ }
+ catch
+ {
+ return GetSystemAccentBrush();
+ }
+ }
+
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ private static bool IsCurrentThemeDark()
+ {
+ try
+ {
+ var app = Avalonia.Application.Current;
+ if (app?.ActualThemeVariant != null)
+ {
+ return app.ActualThemeVariant == ThemeVariant.Dark;
+ }
+ }
+ catch
+ {
+ // Continue to fallback
+ }
+
+ // Fallback: assume light theme
+ return false;
+ }
+
+ private static IBrush GetSystemAccentBrush()
+ {
+ try
+ {
+ if (Avalonia.Application.Current?.TryGetResource("SystemAccentColorBrush",
+ Avalonia.Application.Current.ActualThemeVariant, out var resource) == true)
+ {
+ if (resource is IBrush brush)
+ return brush;
+ }
+
+ if (Avalonia.Application.Current?.TryGetResource("SystemAccentColor",
+ Avalonia.Application.Current.ActualThemeVariant, out var colorResource) == true)
+ {
+ if (colorResource is Color color)
+ return new SolidColorBrush(color);
+ }
+ }
+ catch
+ {
+ // Continue to fallback
+ }
+
+ // Final fallback to a reasonable blue
+ return new SolidColorBrush(Color.Parse("#0078D4"));
+ }
+}
\ No newline at end of file
diff --git a/GalaxyViewer/Converters/BoolToDoubleConverter.cs b/GalaxyViewer/Converters/BoolToDoubleConverter.cs
new file mode 100644
index 0000000..54dbb28
--- /dev/null
+++ b/GalaxyViewer/Converters/BoolToDoubleConverter.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace GalaxyViewer.Converters;
+
+public class BoolToDoubleConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is not bool boolValue || parameter is not string doubleParams)
+ return 1.0;
+ var values = doubleParams.Split(' ', ',');
+ if (values.Length < 2) return 1.0;
+ if (double.TryParse(values[0].Trim(), out var trueValue) &&
+ double.TryParse(values[1].Trim(), out var falseValue))
+ {
+ return boolValue ? trueValue : falseValue;
+ }
+
+ return 1.0;
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/GalaxyViewer/Converters/InversionBoolConverter.cs b/GalaxyViewer/Converters/InversionBoolConverter.cs
new file mode 100644
index 0000000..f6a5b81
--- /dev/null
+++ b/GalaxyViewer/Converters/InversionBoolConverter.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace GalaxyViewer.Converters;
+
+public class InverseBoolConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ => value is bool b ? !b : value;
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ => value is bool b ? !b : value;
+}
\ No newline at end of file
diff --git a/GalaxyViewer/Converters/LocalizedStringConverter.cs b/GalaxyViewer/Converters/LocalizedStringConverter.cs
deleted file mode 100644
index 6a6d96f..0000000
--- a/GalaxyViewer/Converters/LocalizedStringConverter.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System;
-using System.Globalization;
-using Avalonia.Data.Converters;
-using GalaxyViewer.Assets.Localization;
-
-namespace GalaxyViewer.Converters;
-
-public class LocalizedStringConverter : IValueConverter
-{
- public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
- {
- if (parameter is string key) return new LocalizationManager().GetString(key);
- return "Key not found for " + value;
- }
-
- public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
- {
- // Change to en-US if the language is not found
- return value;
- }
-}
\ No newline at end of file
diff --git a/GalaxyViewer/Converters/TabConverters.cs b/GalaxyViewer/Converters/TabConverters.cs
new file mode 100644
index 0000000..39269fb
--- /dev/null
+++ b/GalaxyViewer/Converters/TabConverters.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+
+namespace GalaxyViewer.Converters;
+
+public class BoolToBackgroundConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is bool isActive && isActive)
+ {
+ return new SolidColorBrush(Colors.Transparent);
+ }
+ return new SolidColorBrush(Colors.LightGray);
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+public class BoolToFontWeightConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is bool isActive && isActive)
+ {
+ return FontWeight.SemiBold;
+ }
+ return FontWeight.Normal;
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+public class BoolToForegroundConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is bool isActive && isActive)
+ {
+ return Brushes.Black;
+ }
+ return Brushes.Gray;
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+public class BoolToStatusColorConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is bool isLoggedIn && isLoggedIn)
+ {
+ return new SolidColorBrush(Colors.Green);
+ }
+ return new SolidColorBrush(Colors.Red);
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+public class BoolToStatusTextConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is bool isLoggedIn && isLoggedIn)
+ {
+ return "Connected";
+ }
+ return "Disconnected";
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/GalaxyViewer/Converters/TextDirectionConverter.cs b/GalaxyViewer/Converters/TextDirectionConverter.cs
new file mode 100644
index 0000000..f85404e
--- /dev/null
+++ b/GalaxyViewer/Converters/TextDirectionConverter.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+
+namespace GalaxyViewer.Converters;
+
+public class TextDirectionConverter : IValueConverter
+{
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is not string text || string.IsNullOrEmpty(text))
+ return FlowDirection.LeftToRight;
+
+ // Check if the first character in the text is RTL
+ var firstChar = text.FirstOrDefault(char.IsLetter);
+ if (firstChar == default)
+ return FlowDirection.LeftToRight;
+
+ var unicodeCategory = char.GetUnicodeCategory(firstChar);
+
+ // Check if it's an RTL character
+ if (IsRtlUnicodeCategory(unicodeCategory))
+ return FlowDirection.RightToLeft;
+
+ // Additional check for RTL scripts by Unicode ranges
+ if (IsRtlCharacter(firstChar))
+ return FlowDirection.RightToLeft;
+
+ return FlowDirection.LeftToRight;
+ }
+
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ private static bool IsRtlUnicodeCategory(UnicodeCategory category)
+ {
+ return category == UnicodeCategory.OtherLetter &&
+ char.GetUnicodeCategory('\u0627') == category; // Arabic letter Alef
+ }
+
+ private static bool IsRtlCharacter(char character)
+ {
+ var code = (int)character;
+
+ // Hebrew: U+0590 to U+05FF
+ if (code >= 0x0590 && code <= 0x05FF) return true;
+
+ // Arabic: U+0600 to U+06FF
+ if (code >= 0x0600 && code <= 0x06FF) return true;
+
+ // Arabic Supplement: U+0750 to U+077F
+ if (code >= 0x0750 && code <= 0x077F) return true;
+
+ // Arabic Extended-A: U+08A0 to U+08FF
+ if (code >= 0x08A0 && code <= 0x08FF) return true;
+
+ // Persian/Farsi and Urdu use Arabic script
+ // Thaana (Maldivian): U+0780 to U+07BF
+ if (code >= 0x0780 && code <= 0x07BF) return true;
+
+ // Syriac: U+0700 to U+074F
+ if (code >= 0x0700 && code <= 0x074F) return true;
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/GalaxyViewer/GalaxyViewer.csproj b/GalaxyViewer/GalaxyViewer.csproj
index ac14984..8a5f596 100644
--- a/GalaxyViewer/GalaxyViewer.csproj
+++ b/GalaxyViewer/GalaxyViewer.csproj
@@ -2,26 +2,27 @@
net9.0
latest
- 0.1.0
+ 2025.07.22-test
$(Version)
true
- true
+ Assets\GalaxyViewerLogo.ico
+
+
+
+
PreserveNewest
-
- PreserveNewest
-
-
-
@@ -29,55 +30,34 @@
-
-
-
-
+
+
+
+
+
-
+
+
-
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
-
-
-
-
- ResXFileCodeGenerator
- Strings.Designer.cs
-
-
- ResXFileCodeGenerator
- Strings.en.Designer.cs
-
-
-
-
-
- True
- True
- Strings.resx
-
-
- True
- True
- Strings.en-US.resx
-
+
+
+
-
+
\ No newline at end of file
diff --git a/GalaxyViewer/Models/ChatConversation.cs b/GalaxyViewer/Models/ChatConversation.cs
new file mode 100644
index 0000000..835605e
--- /dev/null
+++ b/GalaxyViewer/Models/ChatConversation.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using OpenMetaverse;
+
+namespace GalaxyViewer.Models;
+
+public class ChatConversation : INotifyPropertyChanged
+{
+ private bool _isTyping;
+
+ public UUID AvatarUuid { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public ChatMessageType MessageType { get; init; }
+ public UUID ParticipantId { get; init; }
+ public UUID GroupId { get; set; }
+ public string? GroupName { get; set; }
+ public UUID? SessionId { get; init; }
+ public string? AvatarImage { get; set; }
+ public ObservableCollection Messages { get; set; } = [];
+ public ObservableCollection TypingUsers { get; set; } = [];
+
+ private DateTime _lastActivity = DateTime.Now;
+ private bool _hasUnreadMessages;
+ private int _unreadCount;
+ private string _lastMessage = string.Empty;
+ private bool _isActive;
+
+ public DateTime LastActivity
+ {
+ get => _lastActivity;
+ set
+ {
+ if (_lastActivity == value) return;
+ _lastActivity = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(LastActivity)));
+ }
+ }
+
+ public bool HasUnreadMessages
+ {
+ get => _hasUnreadMessages;
+ set
+ {
+ if (_hasUnreadMessages == value) return;
+ _hasUnreadMessages = value;
+ PropertyChanged?.Invoke(this,
+ new PropertyChangedEventArgs(nameof(HasUnreadMessages)));
+ }
+ }
+
+ public int UnreadCount
+ {
+ get => _unreadCount;
+ set
+ {
+ if (_unreadCount == value) return;
+ _unreadCount = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UnreadCount)));
+ }
+ }
+
+ public string LastMessage
+ {
+ get => _lastMessage;
+ set
+ {
+ if (_lastMessage == value) return;
+ _lastMessage = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(LastMessage)));
+ }
+ }
+
+ public bool IsActive
+ {
+ get => _isActive;
+ set
+ {
+ if (_isActive == value) return;
+ _isActive = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsActive)));
+ }
+ }
+
+ public bool IsTyping
+ {
+ get => _isTyping;
+ set
+ {
+ if (_isTyping == value) return;
+ _isTyping = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsTyping)));
+ }
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public override string ToString()
+ {
+ return Name;
+ }
+}
\ No newline at end of file
diff --git a/GalaxyViewer/Models/ChatMessage.cs b/GalaxyViewer/Models/ChatMessage.cs
new file mode 100644
index 0000000..d08b44b
--- /dev/null
+++ b/GalaxyViewer/Models/ChatMessage.cs
@@ -0,0 +1,40 @@
+using System;
+using OpenMetaverse;
+
+namespace GalaxyViewer.Models;
+
+public class ChatMessage
+{
+ public string SenderName { get; set; } = string.Empty;
+ public UUID SenderUuid { get; set; }
+ public string Message { get; set; } = string.Empty;
+ public DateTime Timestamp { get; set; } = DateTime.Now;
+ public ChatMessageType MessageType { get; set; }
+ public ChatType ChatType { get; set; } = ChatType.Normal;
+ public ChatSourceType SourceType { get; set; }
+ public ChatAudibleLevel AudibleLevel { get; set; }
+ public UUID GroupId { get; set; }
+ public string? GroupName { get; set; }
+ public bool IsFromSelf { get; set; }
+ public InstantMessageDialog? ImDialog { get; set; }
+ public bool IsSystemMessage { get; set; }
+
+ public string MessageTag
+ {
+ get
+ {
+ if (MessageType == ChatMessageType.System)
+ return "system-message";
+ return IsFromSelf ? "from-self" : "from-other";
+ }
+ }
+}
+
+public enum ChatMessageType
+{
+ LocalChat,
+ InstantMessage,
+ GroupChat,
+ System,
+ Objects
+}
\ No newline at end of file
diff --git a/GalaxyViewer/Models/DebugViewModel.cs b/GalaxyViewer/Models/DebugViewModel.cs
deleted file mode 100644
index f793495..0000000
--- a/GalaxyViewer/Models/DebugViewModel.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-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 814d83d..1b8545c 100644
--- a/GalaxyViewer/Models/GridModel.cs
+++ b/GalaxyViewer/Models/GridModel.cs
@@ -16,4 +16,15 @@ public class GridModel
public string Register { get; set; }
public string Password { get; set; }
public string Version { get; set; }
+ public object IsDefault { get; set; }
+
+ public override bool Equals(object? obj) =>
+ obj is GridModel other && GridName == other.GridName;
+
+ public override int GetHashCode() => GridName?.GetHashCode() ?? 0;
+
+ public override string ToString()
+ {
+ return GridName;
+ }
}
\ No newline at end of file
diff --git a/GalaxyViewer/Models/PreferencesModel.cs b/GalaxyViewer/Models/PreferencesModel.cs
index 1bd7968..991eb63 100644
--- a/GalaxyViewer/Models/PreferencesModel.cs
+++ b/GalaxyViewer/Models/PreferencesModel.cs
@@ -11,5 +11,7 @@ public class PreferencesModel
public string Font { get; set; }
public string Language { get; set; }
public string SelectedGridNick { get; set; }
+ public string AccentColor { get; set; } = "System Default";
public long LastSavedEpoch { get; set; }
+ public string Version { get; set; }
}
\ No newline at end of file
diff --git a/GalaxyViewer/Models/PreferencesOptions.cs b/GalaxyViewer/Models/PreferencesOptions.cs
index f938e9f..da7c772 100644
--- a/GalaxyViewer/Models/PreferencesOptions.cs
+++ b/GalaxyViewer/Models/PreferencesOptions.cs
@@ -4,8 +4,44 @@ namespace GalaxyViewer.Models;
public static class PreferencesOptions
{
- public static readonly List ThemeOptions = ["Light", "Dark", "Default"];
+ public static readonly List ThemeOptions = ["Light", "Dark", "System"];
public static readonly List LoginLocationOptions = ["Home", "Last Location"];
public static readonly List FontOptions = ["Inter", "Atkinson Hyperlegible"];
public static readonly List LanguageOptions = ["en-US"];
+
+ public static readonly List AccentColorOptions = [
+ "System Default", // Uses OS accent color
+ "Blue", // Classic blue
+ "Purple", // Purple accent
+ "Teal", // Teal accent
+ "Green", // Green accent
+ "Orange", // Orange accent
+ "Red", // Red accent
+ "Pink", // Pink accent
+ "Indigo" // Indigo accent
+ ];
+
+ private static readonly Dictionary AccentColors = new()
+ {
+ { "System Default", ("SystemAccent", "SystemAccent") }, // Special key for system accent
+ { "Blue", ("#0078D4", "#60CDFF") }, // Light blue -> Bright blue for dark mode
+ { "Purple", ("#8B5A9F", "#B19CD9") }, // Purple -> Light purple for dark mode
+ { "Teal", ("#0F7173", "#4CC2C4") }, // Teal -> Light teal for dark mode
+ { "Green", ("#107C10", "#6BCF7F") }, // Green -> Light green for dark mode
+ { "Orange", ("#D83B01", "#FF8C00") }, // Orange -> Light orange for dark mode
+ { "Red", ("#D13438", "#FF6B6B") }, // Red -> Light red for dark mode
+ { "Pink", ("#E3008C", "#FF69B4") }, // Pink -> Light pink for dark mode
+ { "Indigo", ("#5C2D91", "#9A7DC4") } // Indigo -> Light indigo for dark mode
+ };
+
+ public static string GetAccentColorForTheme(string accentColorName, bool isDarkTheme)
+ {
+ if (!AccentColors.TryGetValue(accentColorName, out var colorPair))
+ {
+ // Fallback to Blue if accent color not found
+ colorPair = AccentColors["Blue"];
+ }
+
+ return isDarkTheme ? colorPair.Dark : colorPair.Light;
+ }
}
\ No newline at end of file
diff --git a/GalaxyViewer/Models/SessionModel.cs b/GalaxyViewer/Models/SessionModel.cs
index 5eec6dd..e098abd 100644
--- a/GalaxyViewer/Models/SessionModel.cs
+++ b/GalaxyViewer/Models/SessionModel.cs
@@ -1,15 +1,74 @@
using System;
+using System.ComponentModel;
using OpenMetaverse;
namespace GalaxyViewer.Models;
-public class SessionModel
+public class SessionModel : INotifyPropertyChanged
{
- public int Id { get; set; }
- public bool IsLoggedIn { get; set; }
- public string AvatarName { get; set; }
- public UUID AvatarKey { get; set; }
- public int Balance { get; set; }
- public string CurrentLocation { get; set; }
- public string LoginWelcomeMessage { get; set; }
+ private int _id;
+ private string _avatarName;
+ private UUID _avatarKey;
+ private string _currentLocation;
+ private string _loginWelcomeMessage;
+
+ public int Id
+ {
+ get => _id;
+ set
+ {
+ if (_id == value) return;
+ _id = value;
+ OnPropertyChanged(nameof(Id));
+ }
+ }
+
+ public string AvatarName
+ {
+ get => _avatarName;
+ set
+ {
+ if (_avatarName == value) return;
+ _avatarName = value;
+ OnPropertyChanged(nameof(AvatarName));
+ }
+ }
+
+ public UUID AvatarKey
+ {
+ get => _avatarKey;
+ set
+ {
+ if (_avatarKey == value) return;
+ _avatarKey = value;
+ OnPropertyChanged(nameof(AvatarKey));
+ }
+ }
+
+ public string CurrentLocation
+ {
+ get => _currentLocation;
+ set
+ {
+ if (_currentLocation == value) return;
+ _currentLocation = value;
+ OnPropertyChanged(nameof(CurrentLocation));
+ }
+ }
+
+ public string LoginWelcomeMessage
+ {
+ get => _loginWelcomeMessage;
+ set
+ {
+ if (_loginWelcomeMessage == value) return;
+ _loginWelcomeMessage = value;
+ OnPropertyChanged(nameof(LoginWelcomeMessage));
+ }
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void OnPropertyChanged(string propertyName) =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
\ No newline at end of file
diff --git a/GalaxyViewer/Resources/Strings.axaml b/GalaxyViewer/Resources/Strings.axaml
new file mode 100644
index 0000000..d07967a
--- /dev/null
+++ b/GalaxyViewer/Resources/Strings.axaml
@@ -0,0 +1,129 @@
+
+
+ Go back
+ Go Back
+ Cancel editing
+ Location editor
+ Enter location: Region Name, Region/X/Y/Z, secondlife://URL, or maps.secondlife.com URL
+ Go forward
+ Go Forward
+ Search or go to location
+ Search/Go
+ Go to home location
+ Go Home
+ Open address bar settings
+ Settings
+ GalaxyViewer
+ Welcome to GalaxyViewer
+ Chat messages will appear here...
+ You can't send instant messages to objects.
+ Pop Out
+ Pop out chat window
+ Open chat in separate window
+ Send
+ Chat
+ Type a message...
+ GalaxyViewer - Chat
+ Cancel
+ Error
+ Ok
+ Conversations
+ Click to refresh balance
+ About Land
+ About Region
+ Chat
+ Classifieds
+ Communicate
+ Community
+ Create new Landmark Here
+ Dev
+ Developer Tools
+ Events
+ Exit
+ Favorites
+ File
+ Friends
+ Friends List
+ Groups
+ Import Object
+ Landmarks
+ Login
+ Logout
+ Marketplace
+ Mini-Map
+ Nearby Media
+ Nearby Objects
+ Nearby People
+ New Window
+ Objects Nearby
+ People Nearby
+ Preferences
+ Relog
+ Script Editor
+ Set Home to Here
+ Teleport History
+ Teleport Home
+ Upload Blinn-Phong Texture
+ Upload Mesh
+ Upload PBR Material
+ Voice
+ World
+ World Map
+ Login
+ Wrong username or password
+ Grid
+ Login Location
+ Password
+ Login successful
+ Login
+ Username
+ Enter your username
+ Username input field
+ Username
+ Enter your password
+ Password input field
+ Password
+ Log in to GalaxyViewer
+ Login button
+ Chat
+ Developer Tools
+ Friends
+ Groups
+ Landmarks
+ Login
+ Logout
+ Preferences
+ Teleport Home
+ World Map
+ Enter MFA Code:
+ 6-digit code
+ Enter your multi-factor authentication code
+ MFA code input field
+ Submit
+ Submit MFA code
+ Submit button for MFA code
+ Accent Color
+ Appearance
+ Back
+ Font
+ Language
+ Localization
+ Login Location
+ Save Preferences
+ Theme
+ Preferences
+ Select your preferred theme
+ Theme selection
+ Choose an accent color
+ Accent color selection
+ Select your preferred font
+ Font selection
+ Choose your language
+ Language selection
+ Save your preferences
+ Save preferences button
+ Go back to the previous screen
+ Back button
+
+
\ No newline at end of file
diff --git a/GalaxyViewer/Services/ChatService.cs b/GalaxyViewer/Services/ChatService.cs
new file mode 100644
index 0000000..ac55a86
--- /dev/null
+++ b/GalaxyViewer/Services/ChatService.cs
@@ -0,0 +1,675 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia.Threading;
+using GalaxyViewer.Models;
+using OpenMetaverse;
+using Serilog;
+using ChatType = OpenMetaverse.ChatType;
+
+namespace GalaxyViewer.Services;
+
+public sealed class ChatService : IDisposable
+{
+ private readonly GridClient _client;
+ private readonly LiteDbService _dbService;
+ private bool _disposed;
+
+ private bool
+ _hasShownConnectionMessage;
+
+ public ObservableCollection Conversations { get; } = [];
+ public ChatConversation? LocalChatConversation { get; private set; }
+
+ public event EventHandler? MessageReceived;
+ public event EventHandler? ConversationUpdated;
+ public event EventHandler? ActiveConversationChanged;
+
+ private readonly Dictionary> _pendingGroupMessages = new();
+
+ public ChatService(GridClient client, LiteDbService dbService)
+ {
+ _client = client;
+ _dbService = dbService;
+ InitializeLocalChat();
+ RegisterClientEvents();
+
+ _client.Network.EventQueueRunning += OnNetworkConnected;
+ _client.Network.LoginProgress += OnLoginProgress;
+ }
+
+ private void InitializeLocalChat()
+ {
+ LocalChatConversation = new ChatConversation
+ {
+ Name = "Local Chat",
+ MessageType = ChatMessageType.LocalChat,
+ IsActive = true
+ };
+
+ Dispatcher.UIThread.Post(() => { Conversations.Add(LocalChatConversation); });
+ }
+
+ private void RegisterClientEvents()
+ {
+ UnregisterClientEvents();
+
+ _client.Self.ChatFromSimulator += OnChatFromSimulator;
+ _client.Self.IM += OnInstantMessage;
+ }
+
+ private void UnregisterClientEvents()
+ {
+ _client.Self.ChatFromSimulator -= OnChatFromSimulator;
+ _client.Self.IM -= OnInstantMessage;
+ _client.Network.EventQueueRunning -= OnNetworkConnected;
+ _client.Network.LoginProgress -= OnLoginProgress;
+ }
+
+ private void OnChatFromSimulator(object? sender, ChatEventArgs e)
+ {
+ if (e.Type is ChatType.StartTyping or ChatType.StopTyping)
+ {
+ OnLocalChatTyping(e);
+ return;
+ }
+
+ if (ShouldFilterMessage(e.Message))
+ {
+ return;
+ }
+
+ var message = new ChatMessage
+ {
+ SenderName = e.FromName,
+ SenderUuid = e.SourceID,
+ Message = e.Message,
+ MessageType = ChatMessageType.LocalChat,
+ ChatType = e.Type,
+ SourceType = e.SourceType,
+ AudibleLevel = e.AudibleLevel,
+ IsFromSelf = e.SourceID == _client.Self.AgentID
+ };
+
+ if (LocalChatConversation != null) AddMessageToConversation(LocalChatConversation, message);
+ }
+
+ private static bool ShouldFilterMessage(string message)
+ {
+ var lowerMessage = message.ToLowerInvariant();
+
+ var systemFilters = Array.Empty