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