diff --git a/Race Element.UI/App.axaml b/Race Element.UI/App.axaml
new file mode 100644
index 000000000..e111adb7d
--- /dev/null
+++ b/Race Element.UI/App.axaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Race Element.UI/App.axaml.cs b/Race Element.UI/App.axaml.cs
new file mode 100644
index 000000000..7bf412924
--- /dev/null
+++ b/Race Element.UI/App.axaml.cs
@@ -0,0 +1,35 @@
+using Avalonia;
+using Avalonia.Markup.Xaml;
+using Avalonia.Controls.ApplicationLifetimes;
+using RaceElement.UI.ViewModels;
+using RaceElement.UI.Views;
+
+namespace RaceElement.UI;
+public partial class App : Application
+{
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.MainWindow = new MainWindow
+ {
+ DataContext = new MainWindowViewModel(),
+ };
+ }
+ else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
+ {
+ singleViewPlatform.MainView = new MainWindow
+ {
+ DataContext = new MainWindowViewModel()
+ };
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+
+}
\ No newline at end of file
diff --git a/Race Element.UI/Assets/Banner.png b/Race Element.UI/Assets/Banner.png
new file mode 100644
index 000000000..916f5a795
Binary files /dev/null and b/Race Element.UI/Assets/Banner.png differ
diff --git a/Race Element.UI/Assets/acc-list-image.jpg b/Race Element.UI/Assets/acc-list-image.jpg
new file mode 100644
index 000000000..f15880431
Binary files /dev/null and b/Race Element.UI/Assets/acc-list-image.jpg differ
diff --git a/Race Element.UI/Assets/acc.ico b/Race Element.UI/Assets/acc.ico
new file mode 100644
index 000000000..e9f1fe4fd
Binary files /dev/null and b/Race Element.UI/Assets/acc.ico differ
diff --git a/Race Element.UI/Assets/acevo-list-image.jpg b/Race Element.UI/Assets/acevo-list-image.jpg
new file mode 100644
index 000000000..8b05333a1
Binary files /dev/null and b/Race Element.UI/Assets/acevo-list-image.jpg differ
diff --git a/Race Element.UI/Assets/acevo.ico b/Race Element.UI/Assets/acevo.ico
new file mode 100644
index 000000000..a4d130e1f
Binary files /dev/null and b/Race Element.UI/Assets/acevo.ico differ
diff --git a/Race Element.UI/Assets/iracing-list-image.jpg b/Race Element.UI/Assets/iracing-list-image.jpg
new file mode 100644
index 000000000..4bc0aceb2
Binary files /dev/null and b/Race Element.UI/Assets/iracing-list-image.jpg differ
diff --git a/Race Element.UI/Assets/iracing.ico b/Race Element.UI/Assets/iracing.ico
new file mode 100644
index 000000000..024add749
Binary files /dev/null and b/Race Element.UI/Assets/iracing.ico differ
diff --git a/Race Element.UI/Assets/lmu-list-image.png b/Race Element.UI/Assets/lmu-list-image.png
new file mode 100644
index 000000000..9b2a78458
Binary files /dev/null and b/Race Element.UI/Assets/lmu-list-image.png differ
diff --git a/Race Element.UI/Assets/lmu.ico b/Race Element.UI/Assets/lmu.ico
new file mode 100644
index 000000000..f2ef3c309
Binary files /dev/null and b/Race Element.UI/Assets/lmu.ico differ
diff --git a/Race Element.UI/Assets/raceelement.ico b/Race Element.UI/Assets/raceelement.ico
new file mode 100644
index 000000000..2223c4130
Binary files /dev/null and b/Race Element.UI/Assets/raceelement.ico differ
diff --git a/Race Element.UI/Program.cs b/Race Element.UI/Program.cs
new file mode 100644
index 000000000..56a377bc6
--- /dev/null
+++ b/Race Element.UI/Program.cs
@@ -0,0 +1,24 @@
+using System;
+using Avalonia;
+using Avalonia.Styling;
+using Avalonia.ReactiveUI;
+
+namespace RaceElement.UI;
+
+internal sealed class Program
+{
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace()
+ .UseReactiveUI();
+}
diff --git a/Race Element.UI/RaceElement.UI.csproj b/Race Element.UI/RaceElement.UI.csproj
new file mode 100644
index 000000000..69d96a8d6
--- /dev/null
+++ b/Race Element.UI/RaceElement.UI.csproj
@@ -0,0 +1,43 @@
+
+
+ WinExe
+ net8.0
+ enable
+ true
+ app.manifest
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ None
+ All
+
+
+
+
+
+
+
+
+ MainTopMenuView.axaml
+
+
+ ControlButtonsPanelView.axaml
+
+
+
diff --git a/Race Element.UI/Services/GameSelectionService.cs b/Race Element.UI/Services/GameSelectionService.cs
new file mode 100644
index 000000000..f3888e9f2
--- /dev/null
+++ b/Race Element.UI/Services/GameSelectionService.cs
@@ -0,0 +1,75 @@
+// Create a new file: Race Element.UI/Services/GameSelectionService.cs
+using ReactiveUI;
+using System;
+using System.Diagnostics;
+
+namespace RaceElement.UI.Services;
+
+public enum SupportedGame
+{
+ IRacing,
+ ACC,
+ ACEvo,
+ LMU
+}
+
+///
+/// Provides application-wide access to the currently selected game
+///
+public class GameSelectionService : ReactiveObject
+{
+ #region Singleton
+ private static readonly Lazy _instance =
+ new Lazy(() => new GameSelectionService());
+
+ public static GameSelectionService Instance => _instance.Value;
+ #endregion
+
+ #region Events
+ public event Action? GameChanged;
+ #endregion
+
+ #region Properties
+ private SupportedGame _selectedGame = SupportedGame.ACC; // Default game
+
+ public SupportedGame SelectedGame => _selectedGame;
+ #endregion
+
+ #region Constructor
+ // Private constructor for singleton
+ private GameSelectionService()
+ {
+ }
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Updates the selected game and performs any game-specific initialization logic
+ ///
+ /// The game identifier
+ /// True if the selection was valid and processed, false otherwise
+ public bool SelectGame(SupportedGame game)
+ {
+ if (game == _selectedGame)
+ return false;
+
+ _selectedGame = game;
+ GameChanged?.Invoke(game); // Convert SupportedGame enum to string
+ return true;
+ }
+ ///
+ /// Updates the selected game (internal - only used by MainTopMenuViewModel)
+ ///
+ public void UpdateSelectedGame(SupportedGame game)
+ {
+ _selectedGame = game;
+ }
+
+ // Helper methods to check for specific games
+ public bool IsIRacing => SelectedGame == SupportedGame.IRacing;
+ public bool IsACC => SelectedGame == SupportedGame.ACC;
+ public bool IsACEvo => SelectedGame == SupportedGame.ACEvo;
+ public bool IsLMU => SelectedGame == SupportedGame.LMU;
+ #endregion
+}
diff --git a/Race Element.UI/Services/OverlaySelectionService.cs b/Race Element.UI/Services/OverlaySelectionService.cs
new file mode 100644
index 000000000..aafb1d808
--- /dev/null
+++ b/Race Element.UI/Services/OverlaySelectionService.cs
@@ -0,0 +1,216 @@
+using Material.Icons;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace RaceElement.UI.Services;
+
+///
+/// Defines all available overlay types in the application
+///
+public enum OverlayType
+{
+ [Display(Name = "Inputs")]
+ Inputs = 0,
+
+ [Display(Name = "Standings")]
+ Standings = 1,
+
+ [Display(Name = "Relatives")]
+ Relatives = 2,
+
+ [Display(Name = "Laptime")]
+ Laptimes = 3,
+
+ [Display(Name = "Fuel")]
+ Fuel = 4,
+
+ [Display(Name = "Track Map")]
+ TrackMap = 5,
+
+ [Display(Name = "Spotter")]
+ Spotter = 6,
+
+ [Display(Name = "Accelerometer")]
+ Accelerometer = 7,
+
+ [Display(Name = "Average Laptime")]
+ AverageLaptime = 8,
+
+ [Display(Name = "Boost Gauge")]
+ BoostGauge = 9,
+
+ [Display(Name = "Brake Pressure")]
+ BrakePressure = 10
+}
+public static class EnumExtensions
+{
+ public static string GetDisplayName(this Enum enumValue)
+ {
+ var displayAttribute = enumValue.GetType()
+ .GetField(enumValue.ToString())
+ .GetCustomAttribute();
+
+ return displayAttribute?.Name ?? enumValue.ToString();
+ }
+}
+
+///
+/// Service for managing overlay availability and selection across different games
+///
+public class OverlaySelectionService
+{
+ #region Singleton
+ private static readonly Lazy _instance =
+ new Lazy(() => new OverlaySelectionService());
+
+ public static OverlaySelectionService Instance => _instance.Value;
+ #endregion
+
+ #region Game-Specific Overlay Availability
+ // Define which overlays are available for each game
+ private readonly Dictionary> _gameOverlayAvailability;
+ #endregion
+
+ #region Constructor
+ private OverlaySelectionService()
+ {
+ // Initialize availability mappings
+ _gameOverlayAvailability = new Dictionary>
+ {
+ // iRacing supports all overlays
+ [SupportedGame.IRacing] = new HashSet
+ {
+ OverlayType.Inputs,
+ OverlayType.Standings,
+ OverlayType.Relatives,
+ OverlayType.Laptimes,
+ OverlayType.Fuel,
+ OverlayType.TrackMap,
+ OverlayType.Spotter,
+ OverlayType.Accelerometer,
+ OverlayType.AverageLaptime,
+ OverlayType.BoostGauge,
+ OverlayType.BrakePressure
+ },
+
+ // ACC doesn't support BoostGauge and BrakePressure
+ [SupportedGame.ACC] = new HashSet
+ {
+ OverlayType.Inputs,
+ OverlayType.Standings,
+ OverlayType.Relatives,
+ OverlayType.Laptimes,
+ OverlayType.Fuel,
+ OverlayType.TrackMap,
+ OverlayType.Accelerometer,
+ OverlayType.AverageLaptime
+ },
+
+ // AC Evo doesn't support Spotter
+ [SupportedGame.ACEvo] = new HashSet
+ {
+ OverlayType.Inputs,
+ OverlayType.Standings,
+ OverlayType.Relatives,
+ OverlayType.Laptimes,
+ OverlayType.Fuel,
+ OverlayType.TrackMap,
+ OverlayType.Accelerometer,
+ OverlayType.AverageLaptime,
+ OverlayType.BoostGauge,
+ OverlayType.BrakePressure
+ },
+
+ // LMU has limited overlay support
+ [SupportedGame.LMU] = new HashSet
+ {
+ OverlayType.Inputs,
+ OverlayType.Standings,
+ OverlayType.Laptimes,
+ OverlayType.Fuel,
+ OverlayType.Accelerometer
+ }
+ };
+ }
+ #endregion
+
+ #region Public Methods
+ ///
+ /// Checks if an overlay is available for the specified game
+ ///
+ /// The overlay type to check
+ /// The game identifier
+ /// True if the overlay is available, false otherwise
+ public bool IsOverlayAvailableForGame(OverlayType overlayType, SupportedGame game)
+ {
+ if (_gameOverlayAvailability.TryGetValue(game, out var availableItems))
+ {
+ return availableItems.Contains(overlayType);
+ }
+ return false;
+ }
+
+ ///
+ /// Gets all available overlay types for a specific game
+ ///
+ /// The game identifier
+ /// An enumerable of available overlay types
+ public IEnumerable GetAvailableOverlaysForGame(SupportedGame game)
+ {
+ if (_gameOverlayAvailability.TryGetValue(game, out var availableItems))
+ {
+ return availableItems;
+ }
+ return Enumerable.Empty();
+ }
+
+ ///
+ /// Finds the first available overlay for the specified game
+ ///
+ /// The game identifier
+ /// The first available overlay type
+ public OverlayType FindFirstAvailableOverlay(SupportedGame game)
+ {
+ return GetAvailableOverlaysForGame(game).FirstOrDefault();
+ }
+
+ ///
+ /// Gets the Material Icon kind for a specific overlay type
+ ///
+ ///
+ ///
+ public MaterialIconKind GetIconForOverlayType(OverlayType overlayType)
+ {
+ return overlayType switch
+ {
+ OverlayType.Inputs => MaterialIconKind.GraphBar,
+ OverlayType.Standings => MaterialIconKind.ListBox,
+ OverlayType.Relatives => MaterialIconKind.TimelineClock,
+ OverlayType.Laptimes => MaterialIconKind.Timelapse,
+ OverlayType.Fuel => MaterialIconKind.Fuel,
+ OverlayType.TrackMap => MaterialIconKind.Map,
+ OverlayType.Spotter => MaterialIconKind.ViewCarousel,
+ OverlayType.Accelerometer => MaterialIconKind.Speedometer,
+ OverlayType.AverageLaptime => MaterialIconKind.TimeOfDay,
+ OverlayType.BoostGauge => MaterialIconKind.PowerMeter,
+ OverlayType.BrakePressure => MaterialIconKind.StopPause,
+ _ => MaterialIconKind.QuestionMark
+ };
+ }
+
+ ///
+ /// Gets menu name for the overlay type
+ ///
+ ///
+ ///
+ public string GetMenuNameForOverlayType(OverlayType overlayType)
+ {
+ return overlayType.GetDisplayName();
+ }
+ #endregion
+}
diff --git a/Race Element.UI/ViewLocator.cs b/Race Element.UI/ViewLocator.cs
new file mode 100644
index 000000000..7b2fea3a2
--- /dev/null
+++ b/Race Element.UI/ViewLocator.cs
@@ -0,0 +1,38 @@
+using System;
+using RaceElement.UI.ViewModels;
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+
+namespace RaceElement.UI;
+
+public class ViewLocator : IDataTemplate
+{
+ public Control? Build(object? param)
+ {
+ if (param is null)
+ return null;
+
+ var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
+ var type = Type.GetType(name);
+
+ if (type != null)
+ {
+ return (Control)Activator.CreateInstance(type)!;
+ }
+
+ var pageName = param.GetType().FullName!.Replace("ViewModel", "PageView", StringComparison.Ordinal);
+ pageName =pageName.Replace("PageViews", "Views.Pages", StringComparison.Ordinal);
+ var pageType = Type.GetType(pageName);
+
+ if (pageType != null)
+ {
+ return (Control)Activator.CreateInstance(pageType)!;
+ }
+ return new TextBlock { Text = "Not Found: " + name + " or " + pageName};
+ }
+
+ public bool Match(object? data)
+ {
+ return data is ViewModelBase;
+ }
+}
diff --git a/Race Element.UI/ViewModels/DataViewModel.cs b/Race Element.UI/ViewModels/DataViewModel.cs
new file mode 100644
index 000000000..bcbe1081f
--- /dev/null
+++ b/Race Element.UI/ViewModels/DataViewModel.cs
@@ -0,0 +1,7 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels;
+
+public class DataViewModel : ViewModelBase
+{
+}
\ No newline at end of file
diff --git a/Race Element.UI/ViewModels/LiveriesViewModel.cs b/Race Element.UI/ViewModels/LiveriesViewModel.cs
new file mode 100644
index 000000000..b82b88aaa
--- /dev/null
+++ b/Race Element.UI/ViewModels/LiveriesViewModel.cs
@@ -0,0 +1,6 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels;
+public class LiveriesViewModel : ViewModelBase
+{
+}
\ No newline at end of file
diff --git a/Race Element.UI/ViewModels/MainTopMenuViewModel.cs b/Race Element.UI/ViewModels/MainTopMenuViewModel.cs
new file mode 100644
index 000000000..d46f5af51
--- /dev/null
+++ b/Race Element.UI/ViewModels/MainTopMenuViewModel.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Threading.Tasks;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Controls;
+using Avalonia.Themes.Neumorphism.Dialogs;
+using Avalonia.Themes.Neumorphism.Dialogs.Bases;
+
+using System.Reactive;
+using ReactiveUI;
+
+using RaceElement.UI.Services;
+using RaceElement.UI.Views;
+
+namespace RaceElement.UI.ViewModels;
+public class MainTopMenuViewModel : ViewModelBase
+{
+ #region PRIVATE FIELDS
+
+ // Reference to the main window view model for Navigation, DI can be used to access
+ private MainWindowViewModel? _mainWindowViewModel;
+ // Default page
+ private string _currentPageName = "Overlays";
+ #endregion
+
+ #region CONSTRUCTORS
+ public MainTopMenuViewModel()
+ {
+ // Initialize navigation commands
+ NavigateToOverlaysPageCommand = ReactiveCommand.Create(() => { _mainWindowViewModel?.NavigateTo(new OverlaysViewModel()); CurrentPageName = "Overlays"; });
+ NavigateToDataPageCommand = ReactiveCommand.Create(() => { _mainWindowViewModel?.NavigateTo(new DataViewModel()); CurrentPageName = "Data"; });
+ NavigateToSetupsPageCommand = ReactiveCommand.Create(() => { _mainWindowViewModel?.NavigateTo(new SetupsViewModel()); CurrentPageName = "Setups"; });
+ NavigateToLiveriesPageCommand = ReactiveCommand.Create(() => { _mainWindowViewModel?.NavigateTo(new LiveriesViewModel()); CurrentPageName = "Liveries"; });
+ NavigateToToolsPageCommand = ReactiveCommand.Create(() => { _mainWindowViewModel?.NavigateTo(new ToolsViewModel()); CurrentPageName = "Tools"; });
+
+ // Initialize GameSelectionCommand
+ GameSelectionCommand = ReactiveCommand.Create(HandleGameSelection);
+
+ // Pop up settings dialog
+ OpenSettingsDialog = ReactiveCommand.CreateFromTask(async () =>
+ {
+ await ShowSettingsDialogAsync();
+ });
+ }
+ #endregion
+
+ #region PUBIC PROPERTIES
+ public string CurrentPageName
+ {
+ get => _currentPageName;
+ private set => this.RaiseAndSetIfChanged(ref _currentPageName, value);
+ }
+
+ public SupportedGame SelectedGame
+ {
+ get => GameSelectionService.Instance.SelectedGame;
+ private set => GameSelectionService.Instance.UpdateSelectedGame(value);
+ }
+
+ public string CurrentGameIconSource => $"avares://RaceElement.UI/Assets/{SelectedGame.ToString().ToLower()}.ico";
+ #endregion
+
+ #region PUBLIC COMMANDS/METHODS
+ public bool IsCurrentPage(ViewModelBase page)
+ {
+ return _mainWindowViewModel?.CurrentPage == page;
+ }
+
+ // Navigation commands
+ public ReactiveCommand NavigateToOverlaysPageCommand { get; }
+ public ReactiveCommand NavigateToDataPageCommand { get; }
+ public ReactiveCommand NavigateToSetupsPageCommand { get; }
+ public ReactiveCommand NavigateToLiveriesPageCommand { get; }
+ public ReactiveCommand NavigateToToolsPageCommand { get; }
+
+ // Command for game selection from dropdown
+ public ReactiveCommand GameSelectionCommand { get; }
+
+ // Command open settings dialog
+ public ReactiveCommand OpenSettingsDialog { get; }
+
+ // Command to close the application
+ public static ReactiveCommand CloseApplicationCommand => ReactiveCommand.Create(() => { Environment.Exit(0); });
+
+ // Command to minimize the application
+ public static ReactiveCommand MinimizeApplicationCommand => ReactiveCommand.Create(() =>
+ {
+ if (Avalonia.Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
+ {
+ desktopLifetime.MainWindow.WindowState = WindowState.Minimized;
+ }
+ });
+ #endregion
+
+ #region INTERNAL METHODS
+ internal void SetMainWindowViewModel(MainWindowViewModel mainWindowViewModel)
+ {
+ _mainWindowViewModel = mainWindowViewModel;
+ }
+ #endregion
+
+ #region PRIVATE METHODS
+ ///
+ /// Handles the game selection from the dropdown menu.
+ ///
+ ///
+ private void HandleGameSelection(SupportedGame selection)
+ {
+ // Let the service handle game selection and initialization
+ if (GameSelectionService.Instance.SelectGame(selection))
+ {
+ // Update UI only if the game actually changed
+ this.RaisePropertyChanged(nameof(CurrentGameIconSource));
+ }
+ }
+
+ private async Task ShowSettingsDialogAsync()
+ {
+ // Get the main window from the application
+ if (Avalonia.Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop &&
+ desktop.MainWindow is Window mainWindow)
+ {
+ // Initialize the settings view and view model if they don't exist yet
+ var settingsMenuView = new SettingsMenuView();
+ var settingsViewModel = new SettingsMenuViewModel(settingsMenuView);
+
+ settingsMenuView.DataContext = settingsViewModel;
+
+ var dialog = new DialogWindowBase(settingsMenuView);
+
+ // This makes it modal - the main window will be disabled until this dialog is closed
+ DialogResult result = await dialog.ShowDialog(mainWindow);
+
+ // Dialog is about to close
+ if (result != null && result.GetResult == "ok")
+ {
+ }
+ }
+ }
+ #endregion
+
+}
diff --git a/Race Element.UI/ViewModels/MainWindowViewModel.cs b/Race Element.UI/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 000000000..fedaf720e
--- /dev/null
+++ b/Race Element.UI/ViewModels/MainWindowViewModel.cs
@@ -0,0 +1,38 @@
+using ReactiveUI;
+using System.Reactive;
+using Avalonia.Utilities;
+
+namespace RaceElement.UI.ViewModels;
+
+public class MainWindowViewModel : ViewModelBase
+{
+ public MainWindowViewModel()
+ {
+ // Initialize the current page to the overlays page
+ _currentPage = new OverlaysViewModel();
+
+ /// @todo Instead we can use Dependency injection but it has a complex setup
+ /// Can not simply pass this as a parameter to constructor, can be investigated later
+ MainTopMenu = new MainTopMenuViewModel();
+ MainTopMenu.SetMainWindowViewModel(this);
+ }
+
+ public void NavigateTo(ViewModelBase page)
+ {
+ // Block consequent navigation to the same page
+ if (CurrentPage == page) return;
+
+ // Set the current page to the specified page
+ CurrentPage = page;
+ }
+
+ public MainTopMenuViewModel MainTopMenu { get; set; }
+
+ // Default to overlays page
+ private ViewModelBase _currentPage = new OverlaysViewModel();
+ public ViewModelBase CurrentPage
+ {
+ get => _currentPage;
+ set => this.RaiseAndSetIfChanged(ref _currentPage, value);
+ }
+}
diff --git a/Race Element.UI/ViewModels/OverlaySettingViewModels/AccelerometerViewModel.cs b/Race Element.UI/ViewModels/OverlaySettingViewModels/AccelerometerViewModel.cs
new file mode 100644
index 000000000..d4a9ee4b8
--- /dev/null
+++ b/Race Element.UI/ViewModels/OverlaySettingViewModels/AccelerometerViewModel.cs
@@ -0,0 +1,6 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels.OverlaySettingViewModels;
+public class AccelerometerViewModel : OverlaySettingViewModelBase
+{
+}
diff --git a/Race Element.UI/ViewModels/OverlaySettingViewModels/AverageLaptimeViewModel.cs b/Race Element.UI/ViewModels/OverlaySettingViewModels/AverageLaptimeViewModel.cs
new file mode 100644
index 000000000..a1cdfc1e5
--- /dev/null
+++ b/Race Element.UI/ViewModels/OverlaySettingViewModels/AverageLaptimeViewModel.cs
@@ -0,0 +1,7 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels.OverlaySettingViewModels;
+public class AverageLaptimeViewModel : OverlaySettingViewModelBase
+{
+
+}
diff --git a/Race Element.UI/ViewModels/OverlaySettingViewModels/BoostGaugeViewModel.cs b/Race Element.UI/ViewModels/OverlaySettingViewModels/BoostGaugeViewModel.cs
new file mode 100644
index 000000000..16167fae6
--- /dev/null
+++ b/Race Element.UI/ViewModels/OverlaySettingViewModels/BoostGaugeViewModel.cs
@@ -0,0 +1,6 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels.OverlaySettingViewModels;
+public class BoostGaugeViewModel : OverlaySettingViewModelBase
+{
+}
diff --git a/Race Element.UI/ViewModels/OverlaySettingViewModels/BrakePressureViewModel.cs b/Race Element.UI/ViewModels/OverlaySettingViewModels/BrakePressureViewModel.cs
new file mode 100644
index 000000000..ccb1eddc9
--- /dev/null
+++ b/Race Element.UI/ViewModels/OverlaySettingViewModels/BrakePressureViewModel.cs
@@ -0,0 +1,6 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels.OverlaySettingViewModels;
+public class BrakePressureViewModel : OverlaySettingViewModelBase
+{
+}
diff --git a/Race Element.UI/ViewModels/OverlaySettingViewModels/ControlButtonsPanelViewModel.cs b/Race Element.UI/ViewModels/OverlaySettingViewModels/ControlButtonsPanelViewModel.cs
new file mode 100644
index 000000000..03e7ab8f9
--- /dev/null
+++ b/Race Element.UI/ViewModels/OverlaySettingViewModels/ControlButtonsPanelViewModel.cs
@@ -0,0 +1,131 @@
+using Avalonia.Media;
+using ReactiveUI;
+using System;
+using System.Reactive;
+using System.Threading.Tasks;
+
+namespace RaceElement.UI.ViewModels.OverlaySettingViewModels;
+
+public class ControlButtonsPanelViewModel : ViewModelBase
+{
+ #region Private Fields
+ // Reference to the current overlay view model
+ private ViewModelBase _currentOverlayViewModel;
+
+ // Property to determine the favorite icon's color
+ private IBrush _favoriteIconColor = Brushes.White;
+
+ // Property to track if the current overlay is a favorite
+ private bool _isCurrentOverlayFavorite;
+ #endregion
+
+ #region Constructor
+ public ControlButtonsPanelViewModel()
+ {
+ // Initialize commands with their implementations
+ ActivateCommand = ReactiveCommand.CreateFromTask(OnActivateAsync);
+ PreviewNowCommand = ReactiveCommand.CreateFromTask(OnPreviewNowAsync);
+ AddFavoriteCommand = ReactiveCommand.CreateFromTask(OnAddFavoriteAsync);
+ ResetToDefaultCommand = ReactiveCommand.CreateFromTask(OnResetToDefaultAsync);
+ MoveResizeCommand = ReactiveCommand.CreateFromTask(OnMoveResizeAsync);
+ SaveChangesCommand = ReactiveCommand.CreateFromTask(OnSaveChangesAsync);
+ }
+ #endregion
+
+ #region Public Methods
+ // Commands for each button in the control panel
+ public ReactiveCommand ActivateCommand { get; }
+ public ReactiveCommand PreviewNowCommand { get; }
+ public ReactiveCommand AddFavoriteCommand { get; }
+ public ReactiveCommand ResetToDefaultCommand { get; }
+ public ReactiveCommand MoveResizeCommand { get; }
+ public ReactiveCommand SaveChangesCommand { get; }
+
+ public IBrush FavoriteIconColor
+ {
+ get => _favoriteIconColor;
+ private set => this.RaiseAndSetIfChanged(ref _favoriteIconColor, value);
+ }
+
+ public bool IsCurrentOverlayFavorite
+ {
+ get => _isCurrentOverlayFavorite;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref _isCurrentOverlayFavorite, value);
+ UpdateFavoriteIconColor();
+
+ // If the current overlay is any type of OverlaySettingViewModelBase, update its IsFavorite property
+ if (_currentOverlayViewModel is OverlaySettingViewModelBase accelerometerVM)
+ {
+ accelerometerVM.IsFavorite = value;
+ }
+ }
+ }
+
+ public void SetCurrentOverlayViewModel(ViewModelBase viewModel)
+ {
+ _currentOverlayViewModel = viewModel;
+
+ // Update favorite status based on the current view model
+ if (_currentOverlayViewModel is OverlaySettingViewModelBase overlaySettingVM)
+ {
+ IsCurrentOverlayFavorite = overlaySettingVM.IsFavorite;
+
+ overlaySettingVM.WhenAnyValue(vm => vm.IsFavorite)
+ .Subscribe(isFavorite =>
+ {
+ IsCurrentOverlayFavorite = isFavorite;
+ });
+ }
+ else
+ {
+ IsCurrentOverlayFavorite = false;
+ }
+ }
+ #endregion
+
+ #region Private Methods
+ private void UpdateFavoriteIconColor()
+ {
+ FavoriteIconColor = IsCurrentOverlayFavorite ? Brushes.Red : Brushes.White;
+ }
+
+ private async Task OnActivateAsync()
+ {
+ // Implement the logic to activate the overlay
+ await Task.CompletedTask;
+ }
+
+ private async Task OnPreviewNowAsync()
+ {
+ // Implement the logic to preview the overlay
+ await Task.CompletedTask;
+ }
+
+ private async Task OnAddFavoriteAsync()
+ {
+ // Toggle the favorite status
+ IsCurrentOverlayFavorite = !IsCurrentOverlayFavorite;
+ await Task.CompletedTask;
+ }
+
+ private async Task OnResetToDefaultAsync()
+ {
+ // Implement the logic to reset settings to default values
+ await Task.CompletedTask;
+ }
+
+ private async Task OnMoveResizeAsync()
+ {
+ // Implement the logic to reset settings to default values
+ await Task.CompletedTask;
+ }
+
+ private async Task OnSaveChangesAsync()
+ {
+ // Implement the logic to save the current settings
+ await Task.CompletedTask;
+ }
+ #endregion
+}
diff --git a/Race Element.UI/ViewModels/OverlaySettingViewModels/FuelOverlayViewModel.cs b/Race Element.UI/ViewModels/OverlaySettingViewModels/FuelOverlayViewModel.cs
new file mode 100644
index 000000000..ce9a141d7
--- /dev/null
+++ b/Race Element.UI/ViewModels/OverlaySettingViewModels/FuelOverlayViewModel.cs
@@ -0,0 +1,6 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels.OverlaySettingViewModels;
+public class FuelOverlayViewModel : OverlaySettingViewModelBase
+{
+}
diff --git a/Race Element.UI/ViewModels/OverlaySettingViewModels/InputsOverlayViewModel.cs b/Race Element.UI/ViewModels/OverlaySettingViewModels/InputsOverlayViewModel.cs
new file mode 100644
index 000000000..72535d796
--- /dev/null
+++ b/Race Element.UI/ViewModels/OverlaySettingViewModels/InputsOverlayViewModel.cs
@@ -0,0 +1,6 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels.OverlaySettingViewModels;
+public class InputsOverlayViewModel : OverlaySettingViewModelBase
+{
+}
diff --git a/Race Element.UI/ViewModels/OverlaySettingViewModels/LaptimeOverlayViewModel.cs b/Race Element.UI/ViewModels/OverlaySettingViewModels/LaptimeOverlayViewModel.cs
new file mode 100644
index 000000000..b3e757a8d
--- /dev/null
+++ b/Race Element.UI/ViewModels/OverlaySettingViewModels/LaptimeOverlayViewModel.cs
@@ -0,0 +1,6 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels.OverlaySettingViewModels;
+public class LaptimeOverlayViewModel : OverlaySettingViewModelBase
+{
+}
diff --git a/Race Element.UI/ViewModels/OverlaySettingViewModels/OverlaySettingViewModelBase.cs b/Race Element.UI/ViewModels/OverlaySettingViewModels/OverlaySettingViewModelBase.cs
new file mode 100644
index 000000000..ceb489aac
--- /dev/null
+++ b/Race Element.UI/ViewModels/OverlaySettingViewModels/OverlaySettingViewModelBase.cs
@@ -0,0 +1,69 @@
+using Avalonia.Media;
+using Material.Icons;
+using ReactiveUI;
+using System.Collections.ObjectModel;
+
+using RaceElement.UI.Services;
+
+namespace RaceElement.UI.ViewModels.OverlaySettingViewModels;
+public class OverlaySettingViewModelBase : ViewModelBase
+{
+ private bool _isFavorite = false;
+ private OverlayType _type = OverlayType.Inputs;
+ private string _name = "";
+ private MaterialIconKind _iconKind = MaterialIconKind.QuestionMark;
+
+ public OverlaySettingViewModelBase()
+ {
+ }
+
+ ///
+ /// Indicates if this overlay is available for the currently selected game
+ ///
+ public bool IsAvailable => OverlaySelectionService.Instance.IsOverlayAvailableForGame(
+ Type, GameSelectionService.Instance.SelectedGame);
+
+ ///
+ /// Indicates if this overlay is a favorite
+ ///
+ public bool IsFavorite
+ {
+ get => _isFavorite;
+ set => this.RaiseAndSetIfChanged(ref _isFavorite, value);
+ }
+
+ ///
+ /// The type of overlay this item represents
+ ///
+ public OverlayType Type
+ {
+ get => _type;
+ set => this.RaiseAndSetIfChanged(ref _type, value);
+ }
+
+ ///
+ /// Display name of the overlay
+ ///
+ public string Name
+ {
+ get => _name;
+ set => this.RaiseAndSetIfChanged(ref _name, value);
+ }
+
+ ///
+ /// The Material Icons kind to use for this overlay
+ ///
+ public MaterialIconKind IconKind
+ {
+ get => _iconKind;
+ set => this.RaiseAndSetIfChanged(ref _iconKind, value);
+ }
+
+ // Display Settings
+ private bool _isEnabled = true;
+ public bool IsEnabled
+ {
+ get => _isEnabled;
+ set => this.RaiseAndSetIfChanged(ref _isEnabled, value);
+ }
+}
diff --git a/Race Element.UI/ViewModels/OverlaySettingViewModels/RelativesOverlayViewModel.cs b/Race Element.UI/ViewModels/OverlaySettingViewModels/RelativesOverlayViewModel.cs
new file mode 100644
index 000000000..a23addd77
--- /dev/null
+++ b/Race Element.UI/ViewModels/OverlaySettingViewModels/RelativesOverlayViewModel.cs
@@ -0,0 +1,6 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels.OverlaySettingViewModels;
+public class RelativesOverlayViewModel : OverlaySettingViewModelBase
+{
+}
diff --git a/Race Element.UI/ViewModels/OverlaySettingViewModels/SpotterOverlayViewModel.cs b/Race Element.UI/ViewModels/OverlaySettingViewModels/SpotterOverlayViewModel.cs
new file mode 100644
index 000000000..9a23f859e
--- /dev/null
+++ b/Race Element.UI/ViewModels/OverlaySettingViewModels/SpotterOverlayViewModel.cs
@@ -0,0 +1,6 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels.OverlaySettingViewModels;
+public class SpotterOverlayViewModel : OverlaySettingViewModelBase
+{
+}
diff --git a/Race Element.UI/ViewModels/OverlaySettingViewModels/StandingsOverlayViewModel.cs b/Race Element.UI/ViewModels/OverlaySettingViewModels/StandingsOverlayViewModel.cs
new file mode 100644
index 000000000..7c36806c8
--- /dev/null
+++ b/Race Element.UI/ViewModels/OverlaySettingViewModels/StandingsOverlayViewModel.cs
@@ -0,0 +1,6 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels.OverlaySettingViewModels;
+public class StandingsOverlayViewModel : OverlaySettingViewModelBase
+{
+}
diff --git a/Race Element.UI/ViewModels/OverlaySettingViewModels/TrackMapOverlayViewModel.cs b/Race Element.UI/ViewModels/OverlaySettingViewModels/TrackMapOverlayViewModel.cs
new file mode 100644
index 000000000..6126a730f
--- /dev/null
+++ b/Race Element.UI/ViewModels/OverlaySettingViewModels/TrackMapOverlayViewModel.cs
@@ -0,0 +1,6 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels.OverlaySettingViewModels;
+public class TrackMapOverlayViewModel : OverlaySettingViewModelBase
+{
+}
diff --git a/Race Element.UI/ViewModels/OverlaysViewModel.cs b/Race Element.UI/ViewModels/OverlaysViewModel.cs
new file mode 100644
index 000000000..3bc0eb29a
--- /dev/null
+++ b/Race Element.UI/ViewModels/OverlaysViewModel.cs
@@ -0,0 +1,197 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive;
+using ReactiveUI;
+
+using RaceElement.UI.Services;
+using RaceElement.UI.ViewModels.OverlaySettingViewModels;
+using System.Collections.ObjectModel;
+using Material.Icons;
+using System.Linq;
+
+namespace RaceElement.UI.ViewModels;
+
+public class OverlaysViewModel : ViewModelBase
+{
+ #region Private Fields
+ // Index of the currently selected overlay
+ private OverlayType _selectedOverlayType = OverlayType.Inputs;
+ private OverlaySettingViewModelBase _selectedOverlayItem;
+
+ // Dictionary to hold game-specific view models
+ private readonly Dictionary> _gameSpecificViewModels = [];
+
+ // The current game selected in the application
+ private SupportedGame _currentGame = GameSelectionService.Instance.SelectedGame;
+ #endregion
+
+ public OverlaysViewModel()
+ {
+ // Initialize empty view model collections for each supported game
+ _gameSpecificViewModels[SupportedGame.IRacing] = new Dictionary();
+ _gameSpecificViewModels[SupportedGame.ACC] = new Dictionary();
+ _gameSpecificViewModels[SupportedGame.ACEvo] = new Dictionary();
+ _gameSpecificViewModels[SupportedGame.LMU] = new Dictionary();
+
+ // Initialize with the current game from the service
+ _currentGame = GameSelectionService.Instance.SelectedGame;
+
+ // Initialize available overlays
+ UpdateAvailableOverlays();
+
+ // Initialize the selected item
+ _selectedOverlayItem = AvailableOverlays.First();
+ _selectedOverlayType = _selectedOverlayItem.Type;
+
+ // Subscribe to game changes
+ GameSelectionService.Instance.GameChanged += OnGameChanged;
+
+ // When the selected overlay menu changes, notify the CurrentOverlayViewModel property
+ this.WhenAnyValue(x => x._selectedOverlayType)
+ .Subscribe(_ => this.RaisePropertyChanged(nameof(CurrentOverlayViewModel)));
+ }
+
+ #region Public Properties
+ public OverlayType SelectedOverlayType
+ {
+ get => _selectedOverlayType;
+ set
+ {
+ // Validate the enum value before setting it in case overlay type is not available
+ // and unsuported overlay type is selected for a game switch
+ if (Enum.IsDefined(typeof(OverlayType), value))
+ {
+ this.RaiseAndSetIfChanged(ref _selectedOverlayType, value);
+ }
+ else
+ {
+ // If invalid, set to a default value
+ this.RaiseAndSetIfChanged(ref _selectedOverlayType, OverlayType.Inputs);
+ }
+ }
+ }
+
+ public OverlaySettingViewModelBase SelectedOverlayItem
+ {
+ get => _selectedOverlayItem;
+ set
+ {
+ if (value != null)
+ {
+ this.RaiseAndSetIfChanged(ref _selectedOverlayItem, value);
+ // Update the SelectedOverlayType when the item changes
+ SelectedOverlayType = value.Type;
+ }
+ }
+ }
+
+ public ViewModelBase CurrentOverlayViewModel => GetOrCreateViewModel(_selectedOverlayType);
+
+ public ObservableCollection AvailableOverlays { get; } = [];
+
+ #endregion
+
+ #region Private Methods
+ private void UpdateAvailableOverlays()
+ {
+ AvailableOverlays.Clear();
+
+ foreach (var overlayType in OverlaySelectionService.Instance.GetAvailableOverlaysForGame(_currentGame))
+ {
+ AvailableOverlays.Add(new OverlaySettingViewModelBase
+ {
+ Type = overlayType,
+ Name = OverlaySelectionService.Instance.GetMenuNameForOverlayType(overlayType),
+ IconKind = OverlaySelectionService.Instance.GetIconForOverlayType(overlayType)
+ });
+ }
+ }
+
+ ///
+ /// Checks if the overlay is available for the current game
+ ///
+ ///
+ ///
+ private bool IsOverlayAvailable(OverlayType overlayType)
+ {
+ return OverlaySelectionService.Instance.IsOverlayAvailableForGame(overlayType, _currentGame);
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ /// HANDLE SELECTED GAME CHANGES ///
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ private void OnGameChanged(SupportedGame newGame)
+ {
+ // Store current game
+ _currentGame = newGame;
+
+ // Update available overlays for the new game
+ UpdateAvailableOverlays();
+
+ // Force refresh of current view model
+ this.RaisePropertyChanged(nameof(CurrentOverlayViewModel));
+
+ // Make sure selected overlay is available in the new game
+ // If not, select the first available overlay
+ if (!IsOverlayAvailable(_selectedOverlayType))
+ {
+ OverlayType firstAvailable = OverlaySelectionService.Instance.FindFirstAvailableOverlay(_currentGame);
+ SelectedOverlayType = firstAvailable;
+
+ // Find the corresponding item in the available overlays
+ SelectedOverlayItem = AvailableOverlays.FirstOrDefault(o => o.Type == firstAvailable) ??
+ (AvailableOverlays.Count > 0 ? AvailableOverlays[0] : null)!;
+ }
+ else
+ {
+ // If still available, find the item in the new collection
+ SelectedOverlayItem = AvailableOverlays.FirstOrDefault(o => o.Type == _selectedOverlayType) ??
+ (AvailableOverlays.Count > 0 ? AvailableOverlays[0] : null)!;
+ }
+ }
+
+ private ViewModelBase GetOrCreateViewModel(OverlayType type)
+ {
+ // Get the current game's view models
+ var gameViewModels = _gameSpecificViewModels[_currentGame];
+
+ // If we already have a view model for this index and game, return it
+ if (gameViewModels.TryGetValue(type, out var existingViewModel))
+ {
+ return existingViewModel;
+ }
+
+ // Otherwise, create a new one
+ var newViewModel = CreateViewModelForIndex(type);
+ gameViewModels[type] = newViewModel;
+ return newViewModel;
+ }
+
+ private ViewModelBase CreateViewModelForIndex(OverlayType type)
+ {
+ // Create the appropriate view model based on index
+ return type switch
+ {
+ OverlayType.Inputs => new InputsOverlayViewModel(),
+ OverlayType.Standings => new StandingsOverlayViewModel(),
+ OverlayType.Relatives => new RelativesOverlayViewModel(),
+ OverlayType.Laptimes => new LaptimeOverlayViewModel(),
+ OverlayType.Fuel => new FuelOverlayViewModel(),
+ OverlayType.TrackMap => new TrackMapOverlayViewModel(),
+ OverlayType.Spotter => new SpotterOverlayViewModel(),
+ OverlayType.Accelerometer => new AccelerometerViewModel(),
+ OverlayType.AverageLaptime => new AverageLaptimeViewModel(),
+ OverlayType.BoostGauge => new BoostGaugeViewModel(),
+ OverlayType.BrakePressure => new BrakePressureViewModel(),
+ };
+ }
+ #endregion
+
+ #region Cleanup
+ public void Dispose()
+ {
+ // Unsubscribe from game changed event
+ GameSelectionService.Instance.GameChanged -= OnGameChanged;
+ }
+ #endregion
+}
\ No newline at end of file
diff --git a/Race Element.UI/ViewModels/SettingsMenuViewModel.cs b/Race Element.UI/ViewModels/SettingsMenuViewModel.cs
new file mode 100644
index 000000000..c36d89a56
--- /dev/null
+++ b/Race Element.UI/ViewModels/SettingsMenuViewModel.cs
@@ -0,0 +1,128 @@
+using Avalonia.Controls;
+using Avalonia.Themes.Neumorphism.Dialogs;
+using Avalonia.Themes.Neumorphism.Dialogs.Interfaces;
+using Avalonia.Themes.Neumorphism.Dialogs.ViewModels;
+using Avalonia.Themes.Neumorphism.Dialogs.ViewModels.Elements;
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels;
+
+///
+/// View model for the settings menu dialog it should derive from DialogWindowViewModel
+/// to block mainwindow interaction during dialog open
+///
+public class SettingsMenuViewModel : DialogWindowViewModel
+{
+ #region Private Fields
+
+ ///
+ /// Reference to the dialog window
+ ///
+ private readonly Window _window;
+
+ ///
+ /// OK button view model
+ ///
+ private ResultBasedDialogButtonViewModel? _buttonOk;
+
+ ///
+ /// Cancel button view model
+ ///
+ private ResultBasedDialogButtonViewModel? _buttonCancel;
+ #endregion
+
+ #region Public Properties
+
+ ///
+ /// Gets or sets the OK button view model
+ ///
+ public ResultBasedDialogButtonViewModel ButtonOk
+ {
+ get { return _buttonOk; }
+ set
+ {
+ _buttonOk = value;
+ OnPropertyChanged(nameof(ButtonOk));
+ }
+ }
+
+ ///
+ /// Gets or sets the Cancel button view model
+ ///
+ public ResultBasedDialogButtonViewModel ButtonCancel
+ {
+ get { return _buttonCancel; }
+ set
+ {
+ _buttonCancel = value;
+ OnPropertyChanged(nameof(ButtonCancel));
+ }
+ }
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// Initializes a new instance of the class
+ ///
+ /// The dialog window
+ public SettingsMenuViewModel(Window window) : base(window)
+ {
+ _window = window;
+
+ // Create ReactiveCommands that accept ResultBasedDialogButtonViewModel
+ var saveCommand = ReactiveCommand.Create(_ =>
+ {
+ // Save settings here
+ HandleSave();
+ });
+
+ var cancelCommand = ReactiveCommand.Create(_ =>
+ {
+ // Maybe warn for changes will not be saved
+ HandleCancel();
+ });
+
+ // Initialize the OK button
+ ButtonOk = new ResultBasedDialogButtonViewModel(this, "OK", "ok")
+ {
+ Content = "OK",
+ Command = saveCommand
+ };
+
+ // Initialize the Cancel button
+ ButtonCancel = new ResultBasedDialogButtonViewModel(this, "Cancel", "cancel")
+ {
+ Content = "Cancel",
+ Command = cancelCommand
+ };
+ }
+ #endregion
+
+ #region Private Methods
+
+ ///
+ /// Persist all settings
+ ///
+ private void HandleSave()
+ {
+ // Set dialog result
+ DialogResult = new DialogResult("Ok");
+
+ // Close the window directly
+ _window.Close();
+ }
+
+ ///
+ /// Handles the cancel command execution
+ ///
+ private void HandleCancel()
+ {
+ // Set dialog result
+ DialogResult = new DialogResult("Cancel");
+
+ // Close the window directly
+ _window.Close();
+ }
+ #endregion
+}
\ No newline at end of file
diff --git a/Race Element.UI/ViewModels/SetupsViewModel.cs b/Race Element.UI/ViewModels/SetupsViewModel.cs
new file mode 100644
index 000000000..1ed68994d
--- /dev/null
+++ b/Race Element.UI/ViewModels/SetupsViewModel.cs
@@ -0,0 +1,7 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels;
+
+public class SetupsViewModel : ViewModelBase
+{
+}
\ No newline at end of file
diff --git a/Race Element.UI/ViewModels/ToolsViewModel.cs b/Race Element.UI/ViewModels/ToolsViewModel.cs
new file mode 100644
index 000000000..4f4586577
--- /dev/null
+++ b/Race Element.UI/ViewModels/ToolsViewModel.cs
@@ -0,0 +1,7 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels;
+
+public class ToolsViewModel : ViewModelBase
+{
+}
\ No newline at end of file
diff --git a/Race Element.UI/ViewModels/ViewModelBase.cs b/Race Element.UI/ViewModels/ViewModelBase.cs
new file mode 100644
index 000000000..4ec889023
--- /dev/null
+++ b/Race Element.UI/ViewModels/ViewModelBase.cs
@@ -0,0 +1,6 @@
+using ReactiveUI;
+
+namespace RaceElement.UI.ViewModels;
+public class ViewModelBase : ReactiveObject
+{
+}
diff --git a/Race Element.UI/Views/MainTopMenuView.axaml b/Race Element.UI/Views/MainTopMenuView.axaml
new file mode 100644
index 000000000..346239834
--- /dev/null
+++ b/Race Element.UI/Views/MainTopMenuView.axaml
@@ -0,0 +1,205 @@
+
+
+
+
+
+ 128
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/MainTopMenuView.axaml.cs b/Race Element.UI/Views/MainTopMenuView.axaml.cs
new file mode 100644
index 000000000..05a0119d4
--- /dev/null
+++ b/Race Element.UI/Views/MainTopMenuView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace RaceElement.UI.Views;
+
+public partial class MainTopMenuView : UserControl
+{
+ public MainTopMenuView()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/MainWindow.axaml b/Race Element.UI/Views/MainWindow.axaml
new file mode 100644
index 000000000..34ec178e6
--- /dev/null
+++ b/Race Element.UI/Views/MainWindow.axaml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/MainWindow.axaml.cs b/Race Element.UI/Views/MainWindow.axaml.cs
new file mode 100644
index 000000000..9e7f87218
--- /dev/null
+++ b/Race Element.UI/Views/MainWindow.axaml.cs
@@ -0,0 +1,21 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Styling;
+
+using RaceElement.UI.Views.Stylers;
+
+namespace RaceElement.UI.Views;
+
+public partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ // Style the main window and set the default size
+ MainWindowStyler.Decorate(this);
+ Application.Current?.SetValue(
+ ThemeVariantScope.ActualThemeVariantProperty,
+ ThemeVariant.Light);
+ }
+}
diff --git a/Race Element.UI/Views/Pages/DataPageView.axaml b/Race Element.UI/Views/Pages/DataPageView.axaml
new file mode 100644
index 000000000..b145099ae
--- /dev/null
+++ b/Race Element.UI/Views/Pages/DataPageView.axaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/DataPageView.axaml.cs b/Race Element.UI/Views/Pages/DataPageView.axaml.cs
new file mode 100644
index 000000000..db1fcb4af
--- /dev/null
+++ b/Race Element.UI/Views/Pages/DataPageView.axaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels;
+
+namespace RaceElement.UI.Views.Pages;
+
+public partial class DataPageView : ReactiveUserControl
+{
+ public DataPageView()
+ {
+ InitializeComponent();
+ }
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Pages/LiveriesPageView.axaml b/Race Element.UI/Views/Pages/LiveriesPageView.axaml
new file mode 100644
index 000000000..19436629b
--- /dev/null
+++ b/Race Element.UI/Views/Pages/LiveriesPageView.axaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/LiveriesPageView.axaml.cs b/Race Element.UI/Views/Pages/LiveriesPageView.axaml.cs
new file mode 100644
index 000000000..c70b67c6f
--- /dev/null
+++ b/Race Element.UI/Views/Pages/LiveriesPageView.axaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels;
+
+namespace RaceElement.UI.Views.Pages;
+
+public partial class LiveriesPageView : ReactiveUserControl
+{
+ public LiveriesPageView()
+ {
+ InitializeComponent();
+ }
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/AccelerometerView.axaml b/Race Element.UI/Views/Pages/OverlaySettingPages/AccelerometerView.axaml
new file mode 100644
index 000000000..811f01931
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/AccelerometerView.axaml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/AccelerometerView.axaml.cs b/Race Element.UI/Views/Pages/OverlaySettingPages/AccelerometerView.axaml.cs
new file mode 100644
index 000000000..27042d231
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/AccelerometerView.axaml.cs
@@ -0,0 +1,55 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels.OverlaySettingViewModels;
+using RaceElement.UI.Views.OverlaySettingsPages;
+using ReactiveUI;
+
+namespace RaceElement.UI.Views.Pages.OverlaySettingPages;
+
+public partial class AccelerometerView : ReactiveUserControl
+{
+ private ControlButtonsPanelView? _controlButtonsPanelView;
+ public AccelerometerView()
+ {
+ InitializeComponent();
+
+ if (DataContext == null)
+ {
+ DataContext = new AccelerometerViewModel();
+ }
+
+ this.WhenActivated(disposables =>
+ {
+ // Find the ControlButtonsPanelView
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is AccelerometerViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ });
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ // Find the control buttons panel once the view is loaded
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is AccelerometerViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/AverageLaptimeView.axaml b/Race Element.UI/Views/Pages/OverlaySettingPages/AverageLaptimeView.axaml
new file mode 100644
index 000000000..191fdbd92
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/AverageLaptimeView.axaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/AverageLaptimeView.axaml.cs b/Race Element.UI/Views/Pages/OverlaySettingPages/AverageLaptimeView.axaml.cs
new file mode 100644
index 000000000..baf0edd7f
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/AverageLaptimeView.axaml.cs
@@ -0,0 +1,55 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels.OverlaySettingViewModels;
+using RaceElement.UI.Views.OverlaySettingsPages;
+using ReactiveUI;
+
+namespace RaceElement.UI.Views.Pages.OverlaySettingPages;
+
+public partial class AverageLaptimeView : ReactiveUserControl
+{
+ private ControlButtonsPanelView? _controlButtonsPanelView;
+ public AverageLaptimeView()
+ {
+ InitializeComponent();
+
+ if (DataContext == null)
+ {
+ DataContext = new AverageLaptimeViewModel();
+ }
+
+ this.WhenActivated(disposables =>
+ {
+ // Find the ControlButtonsPanelView
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is AverageLaptimeViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ });
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ // Find the control buttons panel once the view is loaded
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is AverageLaptimeViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/BoostGaugeView.axaml b/Race Element.UI/Views/Pages/OverlaySettingPages/BoostGaugeView.axaml
new file mode 100644
index 000000000..dd9ba0af1
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/BoostGaugeView.axaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/BoostGaugeView.axaml.cs b/Race Element.UI/Views/Pages/OverlaySettingPages/BoostGaugeView.axaml.cs
new file mode 100644
index 000000000..2c1be68ab
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/BoostGaugeView.axaml.cs
@@ -0,0 +1,55 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels.OverlaySettingViewModels;
+using RaceElement.UI.Views.OverlaySettingsPages;
+using ReactiveUI;
+
+namespace RaceElement.UI.Views.Pages.OverlaySettingPages;
+
+public partial class BoostGaugeView : ReactiveUserControl
+{
+ private ControlButtonsPanelView? _controlButtonsPanelView;
+ public BoostGaugeView()
+ {
+ InitializeComponent();
+
+ if (DataContext == null)
+ {
+ DataContext = new BoostGaugeViewModel();
+ }
+
+ this.WhenActivated(disposables =>
+ {
+ // Find the ControlButtonsPanelView
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is BoostGaugeViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ });
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ // Find the control buttons panel once the view is loaded
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is BoostGaugeViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/BrakePressureView.axaml b/Race Element.UI/Views/Pages/OverlaySettingPages/BrakePressureView.axaml
new file mode 100644
index 000000000..272f91963
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/BrakePressureView.axaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/BrakePressureView.axaml.cs b/Race Element.UI/Views/Pages/OverlaySettingPages/BrakePressureView.axaml.cs
new file mode 100644
index 000000000..817aa07a6
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/BrakePressureView.axaml.cs
@@ -0,0 +1,55 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels.OverlaySettingViewModels;
+using RaceElement.UI.Views.OverlaySettingsPages;
+using ReactiveUI;
+
+namespace RaceElement.UI.Views.Pages.OverlaySettingPages;
+
+public partial class BrakePressureView : ReactiveUserControl
+{
+ private ControlButtonsPanelView? _controlButtonsPanelView;
+ public BrakePressureView()
+ {
+ InitializeComponent();
+
+ if (DataContext == null)
+ {
+ DataContext = new BrakePressureViewModel();
+ }
+
+ this.WhenActivated(disposables =>
+ {
+ // Find the ControlButtonsPanelView
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is BrakePressureViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ });
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ // Find the control buttons panel once the view is loaded
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is BrakePressureViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/ControlButtonsPanelView.axaml b/Race Element.UI/Views/Pages/OverlaySettingPages/ControlButtonsPanelView.axaml
new file mode 100644
index 000000000..f90415fea
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/ControlButtonsPanelView.axaml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/ControlButtonsPanelView.axaml.cs b/Race Element.UI/Views/Pages/OverlaySettingPages/ControlButtonsPanelView.axaml.cs
new file mode 100644
index 000000000..91521e8ef
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/ControlButtonsPanelView.axaml.cs
@@ -0,0 +1,32 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels.OverlaySettingViewModels;
+using ReactiveUI;
+
+namespace RaceElement.UI.Views.OverlaySettingsPages;
+
+public partial class ControlButtonsPanelView : ReactiveUserControl
+{
+ public ControlButtonsPanelView()
+ {
+ InitializeComponent();
+
+ // Initialize ViewModel if not provided from parent
+ if (DataContext == null)
+ {
+ DataContext = new ControlButtonsPanelViewModel();
+ }
+
+ this.WhenActivated(disposables =>
+ {
+ // Add any reactive disposables if needed
+ });
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/FuelOverlayView.axaml b/Race Element.UI/Views/Pages/OverlaySettingPages/FuelOverlayView.axaml
new file mode 100644
index 000000000..91d41ae47
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/FuelOverlayView.axaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/FuelOverlayView.axaml.cs b/Race Element.UI/Views/Pages/OverlaySettingPages/FuelOverlayView.axaml.cs
new file mode 100644
index 000000000..14bc8254b
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/FuelOverlayView.axaml.cs
@@ -0,0 +1,55 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels.OverlaySettingViewModels;
+using RaceElement.UI.Views.OverlaySettingsPages;
+using ReactiveUI;
+
+namespace RaceElement.UI.Views.Pages.OverlaySettingPages;
+
+public partial class FuelOverlayView : ReactiveUserControl
+{
+ private ControlButtonsPanelView? _controlButtonsPanelView;
+ public FuelOverlayView()
+ {
+ InitializeComponent();
+
+ if (DataContext == null)
+ {
+ DataContext = new FuelOverlayViewModel();
+ }
+
+ this.WhenActivated(disposables =>
+ {
+ // Find the ControlButtonsPanelView
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is FuelOverlayViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ });
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ // Find the control buttons panel once the view is loaded
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is FuelOverlayViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/InputsOverlayView.axaml b/Race Element.UI/Views/Pages/OverlaySettingPages/InputsOverlayView.axaml
new file mode 100644
index 000000000..360c7de00
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/InputsOverlayView.axaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/InputsOverlayView.axaml.cs b/Race Element.UI/Views/Pages/OverlaySettingPages/InputsOverlayView.axaml.cs
new file mode 100644
index 000000000..6659b26ab
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/InputsOverlayView.axaml.cs
@@ -0,0 +1,55 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels.OverlaySettingViewModels;
+using RaceElement.UI.Views.OverlaySettingsPages;
+using ReactiveUI;
+
+namespace RaceElement.UI.Views.Pages.OverlaySettingPages;
+
+public partial class InputsOverlayView : ReactiveUserControl
+{
+ private ControlButtonsPanelView? _controlButtonsPanelView;
+ public InputsOverlayView()
+ {
+ InitializeComponent();
+
+ if (DataContext == null)
+ {
+ DataContext = new InputsOverlayViewModel();
+ }
+
+ this.WhenActivated(disposables =>
+ {
+ // Find the ControlButtonsPanelView
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is InputsOverlayViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ });
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ // Find the control buttons panel once the view is loaded
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is InputsOverlayViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/LaptimeOverlayView.axaml b/Race Element.UI/Views/Pages/OverlaySettingPages/LaptimeOverlayView.axaml
new file mode 100644
index 000000000..8e1b5124c
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/LaptimeOverlayView.axaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/LaptimeOverlayView.axaml.cs b/Race Element.UI/Views/Pages/OverlaySettingPages/LaptimeOverlayView.axaml.cs
new file mode 100644
index 000000000..cfad634eb
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/LaptimeOverlayView.axaml.cs
@@ -0,0 +1,55 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels.OverlaySettingViewModels;
+using RaceElement.UI.Views.OverlaySettingsPages;
+using ReactiveUI;
+
+namespace RaceElement.UI.Views.Pages.OverlaySettingPages;
+
+public partial class LaptimeOverlayView : ReactiveUserControl
+{
+ private ControlButtonsPanelView? _controlButtonsPanelView;
+ public LaptimeOverlayView()
+ {
+ InitializeComponent();
+
+ if (DataContext == null)
+ {
+ DataContext = new LaptimeOverlayViewModel();
+ }
+
+ this.WhenActivated(disposables =>
+ {
+ // Find the ControlButtonsPanelView
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is LaptimeOverlayViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ });
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ // Find the control buttons panel once the view is loaded
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is LaptimeOverlayViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/RelativesOverlayView.axaml b/Race Element.UI/Views/Pages/OverlaySettingPages/RelativesOverlayView.axaml
new file mode 100644
index 000000000..d516cdd8f
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/RelativesOverlayView.axaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/RelativesOverlayView.axaml.cs b/Race Element.UI/Views/Pages/OverlaySettingPages/RelativesOverlayView.axaml.cs
new file mode 100644
index 000000000..cb958bd58
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/RelativesOverlayView.axaml.cs
@@ -0,0 +1,55 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels.OverlaySettingViewModels;
+using RaceElement.UI.Views.OverlaySettingsPages;
+using ReactiveUI;
+
+namespace RaceElement.UI.Views.Pages.OverlaySettingPages;
+
+public partial class RelativesOverlayView : ReactiveUserControl
+{
+ private ControlButtonsPanelView? _controlButtonsPanelView;
+ public RelativesOverlayView()
+ {
+ InitializeComponent();
+
+ if (DataContext == null)
+ {
+ DataContext = new RelativesOverlayViewModel();
+ }
+
+ this.WhenActivated(disposables =>
+ {
+ // Find the ControlButtonsPanelView
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is RelativesOverlayViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ });
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ // Find the control buttons panel once the view is loaded
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is RelativesOverlayViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/SpotterOverlayView.axaml b/Race Element.UI/Views/Pages/OverlaySettingPages/SpotterOverlayView.axaml
new file mode 100644
index 000000000..9a10a66e2
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/SpotterOverlayView.axaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/SpotterOverlayView.axaml.cs b/Race Element.UI/Views/Pages/OverlaySettingPages/SpotterOverlayView.axaml.cs
new file mode 100644
index 000000000..ba7f425a6
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/SpotterOverlayView.axaml.cs
@@ -0,0 +1,55 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels.OverlaySettingViewModels;
+using RaceElement.UI.Views.OverlaySettingsPages;
+using ReactiveUI;
+
+namespace RaceElement.UI.Views.Pages.OverlaySettingPages;
+
+public partial class SpotterOverlayView : ReactiveUserControl
+{
+ private ControlButtonsPanelView? _controlButtonsPanelView;
+ public SpotterOverlayView()
+ {
+ InitializeComponent();
+
+ if (DataContext == null)
+ {
+ DataContext = new SpotterOverlayViewModel();
+ }
+
+ this.WhenActivated(disposables =>
+ {
+ // Find the ControlButtonsPanelView
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is SpotterOverlayViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ });
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ // Find the control buttons panel once the view is loaded
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is SpotterOverlayViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/StandingsOverlayView.axaml b/Race Element.UI/Views/Pages/OverlaySettingPages/StandingsOverlayView.axaml
new file mode 100644
index 000000000..9f18c4ce9
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/StandingsOverlayView.axaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/StandingsOverlayView.axaml.cs b/Race Element.UI/Views/Pages/OverlaySettingPages/StandingsOverlayView.axaml.cs
new file mode 100644
index 000000000..00ce41628
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/StandingsOverlayView.axaml.cs
@@ -0,0 +1,55 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels.OverlaySettingViewModels;
+using RaceElement.UI.Views.OverlaySettingsPages;
+using ReactiveUI;
+
+namespace RaceElement.UI.Views.Pages.OverlaySettingPages;
+
+public partial class StandingsOverlayView : ReactiveUserControl
+{
+ private ControlButtonsPanelView? _controlButtonsPanelView;
+ public StandingsOverlayView()
+ {
+ InitializeComponent();
+
+ if (DataContext == null)
+ {
+ DataContext = new StandingsOverlayViewModel();
+ }
+
+ this.WhenActivated(disposables =>
+ {
+ // Find the ControlButtonsPanelView
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is StandingsOverlayViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ });
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ // Find the control buttons panel once the view is loaded
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is StandingsOverlayViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/TrackMapOverlayView.axaml b/Race Element.UI/Views/Pages/OverlaySettingPages/TrackMapOverlayView.axaml
new file mode 100644
index 000000000..adda1910a
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/TrackMapOverlayView.axaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/OverlaySettingPages/TrackMapOverlayView.axaml.cs b/Race Element.UI/Views/Pages/OverlaySettingPages/TrackMapOverlayView.axaml.cs
new file mode 100644
index 000000000..a7bd807c7
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaySettingPages/TrackMapOverlayView.axaml.cs
@@ -0,0 +1,55 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels.OverlaySettingViewModels;
+using RaceElement.UI.Views.OverlaySettingsPages;
+using ReactiveUI;
+
+namespace RaceElement.UI.Views.Pages.OverlaySettingPages;
+
+public partial class TrackMapOverlayView : ReactiveUserControl
+{
+ private ControlButtonsPanelView? _controlButtonsPanelView;
+ public TrackMapOverlayView()
+ {
+ InitializeComponent();
+
+ if (DataContext == null)
+ {
+ DataContext = new TrackMapOverlayViewModel();
+ }
+
+ this.WhenActivated(disposables =>
+ {
+ // Find the ControlButtonsPanelView
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is TrackMapOverlayViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ });
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ // Find the control buttons panel once the view is loaded
+ _controlButtonsPanelView = this.FindControl("ControlButtons");
+
+ if (_controlButtonsPanelView != null && DataContext is TrackMapOverlayViewModel viewModel)
+ {
+ // Set the current overlay view model on the control buttons panel
+ if (_controlButtonsPanelView.DataContext is ControlButtonsPanelViewModel controlButtonsVM)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(viewModel);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Pages/OverlaysPageView.axaml b/Race Element.UI/Views/Pages/OverlaysPageView.axaml
new file mode 100644
index 000000000..afa582055
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaysPageView.axaml
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/OverlaysPageView.axaml.cs b/Race Element.UI/Views/Pages/OverlaysPageView.axaml.cs
new file mode 100644
index 000000000..a1104a933
--- /dev/null
+++ b/Race Element.UI/Views/Pages/OverlaysPageView.axaml.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Reactive.Linq;
+using System.Reactive.Disposables;
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using ReactiveUI;
+
+using RaceElement.UI.Services;
+using RaceElement.UI.ViewModels;
+using RaceElement.UI.ViewModels.OverlaySettingViewModels;
+using RaceElement.UI.Views.OverlaySettingsPages;
+
+namespace RaceElement.UI.Views.Pages;
+
+public partial class OverlaysPageView : ReactiveUserControl
+{
+ private readonly ControlButtonsPanelView? _controlButtonsPanelView;
+ private ListBox OverlaysList => this.GetControl("OverlayItemsList");
+
+ public OverlaysPageView()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ _controlButtonsPanelView = this.FindControl("GlobalControlButtons");
+
+ this.WhenActivated(disposables =>
+ {
+ // Listen for changes to the ViewModel property
+ this.WhenAnyValue(x => x.ViewModel)
+ .WhereNotNull()
+ .Subscribe(viewModel =>
+ {
+ // Initial setup when ViewModel is set
+ UpdateControlButtons(viewModel.SelectedOverlayType);
+
+ // Subscribe to changes in SelectedOverlayIndex
+ viewModel.WhenAnyValue(vm => vm.SelectedOverlayType)
+ .Subscribe(UpdateControlButtons)
+ .DisposeWith(disposables);
+ })
+ .DisposeWith(disposables);
+ });
+ }
+
+ private void UpdateControlButtons(OverlayType index)
+ {
+ // Update the control buttons based on the selected overlay index
+ if (_controlButtonsPanelView?.DataContext is ControlButtonsPanelViewModel controlButtonsVM &&
+ ViewModel?.CurrentOverlayViewModel != null)
+ {
+ controlButtonsVM.SetCurrentOverlayViewModel(ViewModel.CurrentOverlayViewModel);
+ }
+ }
+}
diff --git a/Race Element.UI/Views/Pages/SetupsPageView.axaml b/Race Element.UI/Views/Pages/SetupsPageView.axaml
new file mode 100644
index 000000000..f4c553fa8
--- /dev/null
+++ b/Race Element.UI/Views/Pages/SetupsPageView.axaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/SetupsPageView.axaml.cs b/Race Element.UI/Views/Pages/SetupsPageView.axaml.cs
new file mode 100644
index 000000000..b3178c3dd
--- /dev/null
+++ b/Race Element.UI/Views/Pages/SetupsPageView.axaml.cs
@@ -0,0 +1,20 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using ReactiveUI;
+using RaceElement.UI.ViewModels;
+
+namespace RaceElement.UI.Views.Pages;
+
+public partial class SetupsPageView : ReactiveUserControl
+{
+ public SetupsPageView()
+ {
+ InitializeComponent();
+ }
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Pages/ToolsPageView.axaml b/Race Element.UI/Views/Pages/ToolsPageView.axaml
new file mode 100644
index 000000000..9081e8e82
--- /dev/null
+++ b/Race Element.UI/Views/Pages/ToolsPageView.axaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/Race Element.UI/Views/Pages/ToolsPageView.axaml.cs b/Race Element.UI/Views/Pages/ToolsPageView.axaml.cs
new file mode 100644
index 000000000..ae543d5d6
--- /dev/null
+++ b/Race Element.UI/Views/Pages/ToolsPageView.axaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using RaceElement.UI.ViewModels;
+
+namespace RaceElement.UI.Views.Pages;
+
+public partial class ToolsPageView : ReactiveUserControl
+{
+ public ToolsPageView()
+ {
+ InitializeComponent();
+ }
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/SettingsMenuView.axaml b/Race Element.UI/Views/SettingsMenuView.axaml
new file mode 100644
index 000000000..d1b4bb6bc
--- /dev/null
+++ b/Race Element.UI/Views/SettingsMenuView.axaml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Race Element.UI/Views/SettingsMenuView.axaml.cs b/Race Element.UI/Views/SettingsMenuView.axaml.cs
new file mode 100644
index 000000000..ca51c13cc
--- /dev/null
+++ b/Race Element.UI/Views/SettingsMenuView.axaml.cs
@@ -0,0 +1,40 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Themes.Neumorphism.Dialogs.Interfaces;
+using Avalonia.Themes.Neumorphism.Dialogs;
+using RaceElement.UI.ViewModels;
+using Avalonia.Markup.Xaml;
+
+namespace RaceElement.UI.Views;
+
+public partial class SettingsMenuView : Window, IDialogWindowResult, IHasNegativeResult
+{
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SettingsMenuView()
+ {
+ AvaloniaXamlLoader.Load(this);
+ WindowStartupLocation = WindowStartupLocation.CenterOwner;
+ SystemDecorations = SystemDecorations.None;
+ ExtendClientAreaToDecorationsHint = true;
+ }
+
+ ///
+ /// Gets the result of the dialog.
+ ///
+ ///
+ public DialogResult GetResult() => (DataContext as SettingsMenuViewModel)?.DialogResult;
+
+ ///
+ /// Ensures that even if the user closes the dialog without explicitly clicking one of the buttons,
+ /// dialog returns a meaningful result.
+ ///
+ ///
+ public void SetNegativeResult(DialogResult result)
+ {
+ if (DataContext is SettingsMenuViewModel viewModel)
+ viewModel.DialogResult = result;
+ }
+}
diff --git a/Race Element.UI/Views/Stylers/ActiveButtonStyler.cs b/Race Element.UI/Views/Stylers/ActiveButtonStyler.cs
new file mode 100644
index 000000000..054cde78f
--- /dev/null
+++ b/Race Element.UI/Views/Stylers/ActiveButtonStyler.cs
@@ -0,0 +1,37 @@
+using Avalonia;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using System;
+using System.Globalization;
+
+namespace RaceElement.UI.Views.Stylers;
+
+public sealed class ActiveButtonStyler : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ // Current page name from binding
+ string currentPage = value as string;
+
+ // Button's page name from parameter
+ string buttonPage = parameter as string;
+
+ // Check if this button matches the current page
+ bool isSelected = currentPage == buttonPage;
+
+ // Return appropriate value based on property type
+ if (targetType == typeof(Thickness))
+ return isSelected ? new Thickness(4) : new Thickness(1);
+
+ if (targetType == typeof(IBrush))
+ return isSelected ? new SolidColorBrush(Colors.LightSteelBlue) : new SolidColorBrush(Colors.Transparent);
+
+ return null;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value?.Equals(true) == true ? parameter : BindingOperations.DoNothing;
+ }
+}
\ No newline at end of file
diff --git a/Race Element.UI/Views/Stylers/EnumToIndexConverter.cs b/Race Element.UI/Views/Stylers/EnumToIndexConverter.cs
new file mode 100644
index 000000000..4df2e6431
--- /dev/null
+++ b/Race Element.UI/Views/Stylers/EnumToIndexConverter.cs
@@ -0,0 +1,37 @@
+using Avalonia;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using System;
+using System.Globalization;
+
+using RaceElement.UI.Services;
+
+namespace RaceElement.UI.Views.Stylers;
+
+public class EnumToIndexConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is OverlayType overlayType)
+ return (int)overlayType;
+ return 0;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ // Check if the value is a valid index
+ if (value is int index)
+ {
+ // Handle special case: -1 (no selection) should return a default value
+ if (index == -1)
+ return OverlayType.Inputs;
+
+ // Check if it's a valid enum value
+ if (Enum.IsDefined(typeof(OverlayType), index))
+ return (OverlayType)index;
+ }
+
+ return OverlayType.Inputs;
+ }
+}
diff --git a/Race Element.UI/Views/Stylers/GameSelectionIconStyler.cs b/Race Element.UI/Views/Stylers/GameSelectionIconStyler.cs
new file mode 100644
index 000000000..afa6bad01
--- /dev/null
+++ b/Race Element.UI/Views/Stylers/GameSelectionIconStyler.cs
@@ -0,0 +1,46 @@
+using Avalonia;
+using Avalonia.Data.Converters;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using System;
+using System.Globalization;
+using System.IO;
+
+namespace RaceElement.UI.Views.Stylers;
+
+public class GameSelectionIconStyler : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is string imagePath)
+ {
+ try
+ {
+ // Handle asset paths
+ if (imagePath.StartsWith("avares://"))
+ {
+ var uri = new Uri(imagePath);
+
+ using var stream = AssetLoader.Open(uri);
+ return new Bitmap(stream);
+ }
+
+ // Handle local files
+ return new Bitmap(imagePath);
+ }
+ catch (Exception ex)
+ {
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+}
+
+
diff --git a/Race Element.UI/Views/Stylers/MainWindowStyler.cs b/Race Element.UI/Views/Stylers/MainWindowStyler.cs
new file mode 100644
index 000000000..6a494c161
--- /dev/null
+++ b/Race Element.UI/Views/Stylers/MainWindowStyler.cs
@@ -0,0 +1,137 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Platform;
+
+namespace RaceElement.UI.Views.Stylers;
+
+///
+/// Sets the default size of the window.
+///
+///
+///
+public readonly struct WindowSize(int width, int height)
+{
+ public readonly double DefaultWidth { get; } = width;
+ public readonly double DefaultHeight { get; } = height;
+}
+
+internal class MainWindowStyler
+{
+ public static void Decorate(Window window)
+ {
+ _mainWindow = window;
+
+ // Initialize a WindowSize struct with 1200 and 768 values
+ _windowSize = new WindowSize(1200, 768);
+
+ // Assign the values to the mainWindow attributes
+ _mainWindow.Width = _windowSize.DefaultWidth;
+ _mainWindow.Height = _windowSize.DefaultHeight;
+ _mainWindow.MinWidth = _windowSize.DefaultWidth;
+ _mainWindow.MinHeight = _windowSize.DefaultHeight;
+
+ // Window configuration
+ _mainWindow.Background = Brushes.Transparent;
+ _mainWindow.ExtendClientAreaToDecorationsHint = true;
+ _mainWindow.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome;
+ _mainWindow.ExtendClientAreaTitleBarHeightHint = -1;
+ _mainWindow.SystemDecorations = SystemDecorations.None;
+ _mainWindow.WindowStartupLocation = WindowStartupLocation.CenterScreen;
+
+ // To make mainwindow draggable
+ _mainWindow.Loaded += MainWindow_Loaded;
+ }
+
+ private static void MainWindow_Loaded(object? sender, RoutedEventArgs e)
+ {
+ var TopMenuBar = _mainWindow.FindControl("TopMenuBar");
+ // Access TopMenuBar directly by its generated field x:Name
+ if (TopMenuBar != null)
+ {
+ TopMenuBar.PointerPressed += (s, args) =>
+ {
+ // Get the source of the event
+ // Temporary solution to game seleciton menu resulting triggers movement
+ if (args.Source is Control sourceControl)
+ {
+ // Check if the source or any of its ancestors is a MenuItem or part of game selector menu
+ bool isMenuItemOrGameSelector = IsPartOfGameSelectorMenu(sourceControl);
+
+ if (isMenuItemOrGameSelector)
+ {
+ // Skip dragging if the click is on a menu item or the game selector
+ args.Handled = true;
+ return;
+ }
+ }
+
+ _isDragging = true;
+ _lastPosition = args.GetPosition(_mainWindow);
+ _mainWindow.PointerReleased += PointerReleasedHandler;
+ _mainWindow.PointerMoved += PointerMovedHandler;
+ };
+ }
+ }
+
+ // Temporary solution to game seleciton menu resulting triggers movement
+ private static bool IsPartOfGameSelectorMenu(Control control)
+ {
+ // Check current control
+ if (control is MenuItem || control.Name == "GameSelectionMenu")
+ {
+ return true;
+ }
+
+ // Check parent hierarchy
+ var parent = control.Parent as Control;
+ while (parent != null)
+ {
+ if (parent is MenuItem || parent.Name == "GameSelectionMenu")
+ {
+ return true;
+ }
+ parent = parent.Parent as Control;
+ }
+
+ return false;
+ }
+
+ private static void PointerReleasedHandler(object? sender, PointerReleasedEventArgs e)
+ {
+ _isDragging = false;
+ _mainWindow.PointerReleased -= PointerReleasedHandler;
+ _mainWindow.PointerMoved -= PointerMovedHandler;
+ }
+
+ private static void PointerMovedHandler(object? sender, PointerEventArgs e)
+ {
+ if (_isDragging)
+ {
+ var currentPoint = e.GetPosition(_mainWindow);
+ var offset = currentPoint - _lastPosition;
+
+ // Fix: Use _mainWindow.Position to update the window's position
+ _mainWindow.Position = new PixelPoint(
+ _mainWindow.Position.X + (int)offset.X,
+ _mainWindow.Position.Y + (int)offset.Y
+ );
+ }
+ }
+
+ /// ------------------------------------------------------------------------------
+ /// Private Fields
+ /// ------------------------------------------------------------------------------
+
+ // Fields to track the dragging state and last position of the pointer
+ private static bool _isDragging = false;
+ private static Point _lastPosition;
+
+ // WindowSize struct to hold the default window size
+ private static WindowSize _windowSize;
+
+ // Reference to the main window
+ private static Window _mainWindow;
+}
diff --git a/Race Element.UI/app.manifest b/Race Element.UI/app.manifest
new file mode 100644
index 000000000..a18ea31ab
--- /dev/null
+++ b/Race Element.UI/app.manifest
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Race Element.sln b/Race Element.sln
index 8c5c2f65b..21d2309d5 100644
--- a/Race Element.sln
+++ b/Race Element.sln
@@ -31,6 +31,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Race Element.HUD.Common", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Race Element.Graph", "Race Element.Graph\Race Element.Graph.csproj", "{E3205DD5-EF7D-427F-916C-E95531877AD6}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RaceElement.UI", "Race Element.UI\RaceElement.UI.csproj", "{FEF1EAFA-80D0-928B-79B0-879F7FE25C67}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug Minimized|Any CPU = Debug Minimized|Any CPU
@@ -260,6 +262,24 @@ Global
{E3205DD5-EF7D-427F-916C-E95531877AD6}.Release|x64.Build.0 = Release|Any CPU
{E3205DD5-EF7D-427F-916C-E95531877AD6}.Release|x86.ActiveCfg = Release|Any CPU
{E3205DD5-EF7D-427F-916C-E95531877AD6}.Release|x86.Build.0 = Release|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Debug Minimized|Any CPU.ActiveCfg = Debug|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Debug Minimized|Any CPU.Build.0 = Debug|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Debug Minimized|x64.ActiveCfg = Debug|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Debug Minimized|x64.Build.0 = Debug|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Debug Minimized|x86.ActiveCfg = Debug|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Debug Minimized|x86.Build.0 = Debug|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Debug|x64.Build.0 = Debug|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Debug|x86.Build.0 = Debug|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Release|x64.ActiveCfg = Release|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Release|x64.Build.0 = Release|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Release|x86.ActiveCfg = Release|Any CPU
+ {FEF1EAFA-80D0-928B-79B0-879F7FE25C67}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE