diff --git a/MagicChatboxV2/App.xaml b/MagicChatboxV2/App.xaml
new file mode 100644
index 0000000..9c15297
--- /dev/null
+++ b/MagicChatboxV2/App.xaml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/MagicChatboxV2/App.xaml.cs b/MagicChatboxV2/App.xaml.cs
new file mode 100644
index 0000000..1144167
--- /dev/null
+++ b/MagicChatboxV2/App.xaml.cs
@@ -0,0 +1,100 @@
+using Microsoft.Extensions.DependencyInjection;
+using System.Windows;
+using MagicChatboxV2.Services;
+using MagicChatboxV2.Helpers;
+using MagicChatboxV2.Extensions;
+using System.Reflection;
+using Serilog;
+using System.Windows.Threading;
+using MagicChatboxV2.UIVM.Windows;
+
+namespace MagicChatboxV2
+{
+ public partial class App : Application
+ {
+ private IServiceProvider serviceProvider;
+ public delegate PrimaryInterface PrimaryInterfaceFactory();
+
+
+ public App()
+ {
+ // Configure the logger
+ Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Debug()
+ .WriteTo.Console()
+ .WriteTo.File("logs/application.log", rollingInterval: RollingInterval.Day)
+ .CreateLogger();
+
+ // Set the shutdown mode to explicit
+ Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
+
+ // Handle unhandled exceptions in the Dispatcher
+ DispatcherUnhandledException += App_DispatcherUnhandledException;
+
+ // Handle unhandled exceptions in the AppDomain
+ AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
+ }
+
+ // Handler for unhandled exceptions in the Dispatcher
+ private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
+ {
+ // Log the exception
+ Log.Error(e.Exception, "An unhandled Dispatcher exception occurred");
+
+ // Show a custom error dialog with the exception details
+ var errorDialog = new CustomErrorDialog(e.Exception.Message, e.Exception.StackTrace);
+ errorDialog.ShowDialog();
+
+ // Mark the exception as handled
+ e.Handled = true;
+ }
+
+ // Handler for unhandled exceptions in the AppDomain
+ private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
+ {
+ // Log the exception
+ Log.Error((Exception)e.ExceptionObject, "An unhandled Domain exception occurred");
+
+ // Show a custom error dialog with the exception details
+ var errorDialog = new CustomErrorDialog(((Exception)e.ExceptionObject).Message, ((Exception)e.ExceptionObject).StackTrace);
+ errorDialog.ShowDialog();
+ }
+
+ // Configure and build the service provider
+ private IServiceProvider ConfigureServices()
+ {
+ var services = new ServiceCollection();
+
+ // Register services and modules as before
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddModules(Assembly.GetExecutingAssembly());
+
+
+
+ // Register ModuleManagerService
+ services.AddSingleton();
+
+ services.AddSingleton();
+ services.AddSingleton(serviceProvider => () => serviceProvider.GetRequiredService());
+
+
+ return services.BuildServiceProvider();
+ }
+
+
+ protected override void OnStartup(StartupEventArgs e)
+ {
+ base.OnStartup(e);
+ serviceProvider = ConfigureServices();
+
+ // Get the StartupHelper service from the service provider
+ var startupHelper = serviceProvider.GetService();
+
+ // Start the application logic using the StartupHelper
+ startupHelper?.Start();
+ }
+
+ }
+}
diff --git a/MagicChatboxV2/AssemblyInfo.cs b/MagicChatboxV2/AssemblyInfo.cs
new file mode 100644
index 0000000..b0ec827
--- /dev/null
+++ b/MagicChatboxV2/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/MagicChatboxV2/Extensions/ServiceCollectionExtensions.cs b/MagicChatboxV2/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..76e98b6
--- /dev/null
+++ b/MagicChatboxV2/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,23 @@
+using Microsoft.Extensions.DependencyInjection;
+using System.Reflection;
+
+namespace MagicChatboxV2.Extensions
+{
+ public static class ServiceCollectionExtensions
+ {
+ public static IServiceCollection AddModules(this IServiceCollection services, Assembly assembly)
+ {
+ // Find all types in the assembly that implement IModule and are class types
+ var moduleTypes = assembly.GetTypes()
+ .Where(t => typeof(UIVM.Models.IModule).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
+
+ // Register each found module type with the services collection
+ foreach (var type in moduleTypes)
+ {
+ services.AddTransient(typeof(UIVM.Models.IModule), type);
+ }
+
+ return services;
+ }
+ }
+}
diff --git a/MagicChatboxV2/Helpers/StartupHelper.cs b/MagicChatboxV2/Helpers/StartupHelper.cs
new file mode 100644
index 0000000..2b2ded0
--- /dev/null
+++ b/MagicChatboxV2/Helpers/StartupHelper.cs
@@ -0,0 +1,117 @@
+using MagicChatboxV2.Services;
+using MagicChatboxV2.UIVM.Windows;
+using Microsoft.Extensions.DependencyInjection;
+using Serilog;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using static MagicChatboxV2.App;
+
+namespace MagicChatboxV2.Helpers
+{
+ public class StartupHelper
+ {
+ private readonly IServiceProvider serviceProvider;
+ private readonly PrimaryInterfaceFactory _mainWindowFactory;
+ private LoadingWindow loadingWindow;
+
+ public StartupHelper(IServiceProvider serviceProvider, PrimaryInterfaceFactory mainWindowFactory)
+ {
+ this.serviceProvider = serviceProvider;
+ this._mainWindowFactory = mainWindowFactory;
+ }
+
+ // Start method to initiate the startup process
+ public void Start()
+ {
+ try
+ {
+ ShowLoadingWindow();
+ InitializeServices();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error during service initialization");
+ MessageBox.Show($"Failed to initialize services: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ CloseLoadingWindow();
+ Application.Current.Shutdown();
+ return;
+ }
+
+ CloseLoadingWindow();
+
+ try
+ {
+ var vrChatService = serviceProvider.GetService();
+ vrChatService.OnVRChatStarted += OnVRChatStarted;
+ vrChatService.StartMonitoring();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Failed to start VRChat monitoring");
+ MessageBox.Show($"Failed to start VRChat monitoring: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+
+ }
+ }
+
+ // Show the loading window
+ private void ShowLoadingWindow()
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ loadingWindow = new LoadingWindow();
+ loadingWindow.Show();
+ });
+ }
+
+ // Initialize the services
+ private void InitializeServices()
+ {
+ UpdateProgress(0, "Initializing System Tray...");
+ var trayService = serviceProvider.GetService();
+ trayService.InitializeTrayIcon();
+ UpdateProgress(50, "System Tray Initialized.");
+
+ UpdateProgress(100, "Initialization Complete.");
+ }
+
+ // Update the progress of the loading window
+ private void UpdateProgress(double progress, string status)
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ loadingWindow.UpdateProgress(progress, status);
+ });
+ }
+
+ // Close the loading window
+ private void CloseLoadingWindow()
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ loadingWindow.Close();
+ });
+ }
+
+ // Event handler for when VRChat is started
+ private void OnVRChatStarted()
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ var mainWindow = _mainWindowFactory();
+ if (!mainWindow.IsVisible)
+ {
+ mainWindow.Show();
+ }
+ else
+ {
+ mainWindow.Activate();
+ }
+ });
+ }
+
+ }
+}
diff --git a/MagicChatboxV2/MagicChatboxV2.csproj b/MagicChatboxV2/MagicChatboxV2.csproj
new file mode 100644
index 0000000..342cb73
--- /dev/null
+++ b/MagicChatboxV2/MagicChatboxV2.csproj
@@ -0,0 +1,33 @@
+
+
+
+ WinExe
+ net7.0-windows
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MagicChatboxV2/PrimaryInterface.xaml b/MagicChatboxV2/PrimaryInterface.xaml
new file mode 100644
index 0000000..5699ab7
--- /dev/null
+++ b/MagicChatboxV2/PrimaryInterface.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
diff --git a/MagicChatboxV2/PrimaryInterface.xaml.cs b/MagicChatboxV2/PrimaryInterface.xaml.cs
new file mode 100644
index 0000000..096dbda
--- /dev/null
+++ b/MagicChatboxV2/PrimaryInterface.xaml.cs
@@ -0,0 +1,58 @@
+using MagicChatboxV2.Services;
+using Microsoft.Extensions.DependencyInjection;
+using System.ComponentModel;
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+
+namespace MagicChatboxV2
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class PrimaryInterface : Window
+ {
+ private readonly ModuleManagerService _moduleManagerService;
+
+ // Constructor injection of ModuleManagerService
+ public PrimaryInterface(ModuleManagerService moduleManagerService)
+ {
+ InitializeComponent();
+ _moduleManagerService = moduleManagerService;
+ }
+
+ private void GET_Click(object sender, RoutedEventArgs e)
+ {
+ // Retrieve formatted outputs from all active modules and display them
+ var formattedOutputs = _moduleManagerService.GetFormattedOutputs();
+ // Assuming you want to display these outputs in a TextBlock or similar control
+ // For demonstration, updating the Button content; adjust based on your UI design
+ GetFormattedOutput.Text = formattedOutputs;
+ }
+
+ protected override void OnClosing(CancelEventArgs e)
+ {
+ e.Cancel = true; // Cancel the closing
+ this.Hide(); // Hide the window instead of closing it
+ base.OnClosing(e);
+ }
+
+ private void disposemodules_Click(object sender, RoutedEventArgs e)
+ {
+ _moduleManagerService.DisposeModules();
+ }
+
+ private void startmodules_Click(object sender, RoutedEventArgs e)
+ {
+ _moduleManagerService.InitializeModules();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/MagicChatboxV2/Services/ModuleManagerService.cs b/MagicChatboxV2/Services/ModuleManagerService.cs
new file mode 100644
index 0000000..9c864b6
--- /dev/null
+++ b/MagicChatboxV2/Services/ModuleManagerService.cs
@@ -0,0 +1,88 @@
+using MagicChatboxV2.UIVM.Models;
+using Microsoft.Extensions.DependencyInjection;
+using Serilog;
+
+namespace MagicChatboxV2.Services;
+
+public class ModuleManagerService : IDisposable
+{
+ private readonly IServiceProvider serviceProvider;
+ private readonly List modules = new List();
+
+ public ModuleManagerService(IServiceProvider serviceProvider)
+ {
+ this.serviceProvider = serviceProvider;
+ InitializeModules();
+ }
+
+ public void InitializeModules()
+ {
+ // Directly get the instances of all modules implementing IModule
+ var moduleInstances = serviceProvider.GetServices();
+
+ foreach (var module in moduleInstances)
+ {
+ try
+ {
+ module.Initialize();
+ module.LoadState();
+ modules.Add(module);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Failed to initialize module {module.ModuleName}");
+ // Optionally, show a user-friendly message or take corrective action
+ }
+ }
+ }
+
+
+ // Dispose of the modules when the service is disposed
+ public void Dispose()
+ {
+ DisposeModules();
+ }
+
+ // Start or stop updates for all modules based on their enabled state
+ public void StartModuleUpdates()
+ {
+ foreach (var module in modules.Where(m => m.IsEnabled))
+ {
+ module.StartUpdates();
+ }
+ }
+
+ // Start or stop updates for all modules based on their enabled state
+ public void StopModuleUpdates()
+ {
+ foreach (var module in modules)
+ {
+ module.StopUpdates();
+ }
+ }
+
+ // Update data for all active modules
+ public void UpdateModuleData()
+ {
+ foreach (var module in modules.Where(m => m.IsActive))
+ {
+ module.UpdateData();
+ }
+ }
+
+
+ public string GetFormattedOutputs()
+ {
+ // Concatenate or cycle through outputs based on your strategy
+ return string.Join("\n", modules.Where(m => m.IsActive).Select(m => m.GetFormattedOutput()));
+ }
+
+ public void DisposeModules()
+ {
+ foreach (var module in modules)
+ {
+ module.Dispose();
+ }
+ modules.Clear();
+ }
+ }
diff --git a/MagicChatboxV2/Services/Modules/CurrentTimeModule.cs b/MagicChatboxV2/Services/Modules/CurrentTimeModule.cs
new file mode 100644
index 0000000..e1c6eef
--- /dev/null
+++ b/MagicChatboxV2/Services/Modules/CurrentTimeModule.cs
@@ -0,0 +1,236 @@
+using MagicChatboxV2.UIVM.Models;
+using Serilog;
+using System;
+using System.Globalization;
+using System.Linq;
+
+namespace MagicChatboxV2.Services.Modules
+{
+ public class CurrentTimeSettings : ISettings
+ {
+ public enum TimeZone
+ {
+ UTC,
+ EST,
+ CST,
+ PST,
+ CET,
+ AEST,
+ GMT,
+ IST,
+ JST
+ }
+
+ public TimeZone SelectedTimeZone { get; set; } = TimeZone.EST;
+ public bool AutoSetDaylight { get; set; } = true;
+ public bool ManualDstAdjustment { get; set; } = false;
+ public bool ConvertTimeToTimeZone { get; set; } = false;
+ public bool Time24H { get; set; } = true;
+
+ public void Dispose() { }
+ }
+
+ public class CurrentTimeModule : IModule
+ {
+ public string ModuleName => "Current time";
+ public bool IsActive { get; set; }
+ public bool IsEnabled { get; set; }
+ public bool IsEnabled_VR { get; set; }
+ public bool IsEnabled_DESKTOP { get; set; }
+
+
+
+ public DateTime LastUpdated { get; private set; }
+
+ public event EventHandler DataUpdated;
+
+ public ISettings Settings { get; set; }
+ public int ModulePosition { get; set; }
+ public int ModuleMemberGroupNumbers { get; set; }
+
+ public CurrentTimeModule()
+ {
+ Settings = new CurrentTimeSettings();
+ IsActive = true;
+ IsEnabled = true;
+ IsEnabled_VR = true;
+ IsEnabled_DESKTOP = true;
+ }
+
+ public void Initialize()
+ {
+ Console.WriteLine($"{ModuleName} initialized.");
+ }
+
+ public void StartUpdates()
+ {
+ // Start background updates if required
+ }
+
+ public void StopUpdates()
+ {
+ // Stop background updates if required
+ }
+
+ public void UpdateData()
+ {
+ try
+ {
+ LastUpdated = DateTime.Now;
+ DataUpdated?.Invoke(this, EventArgs.Empty);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Failed to update data for {ModuleName}");
+ }
+
+ }
+
+ public string GetFormattedOutput()
+ {
+ try
+ {
+ var settings = (CurrentTimeSettings)Settings;
+ DateTimeOffset localDateTime = DateTimeOffset.Now;
+ TimeZoneInfo timeZoneInfo = GetTimeZoneInfo(settings.SelectedTimeZone);
+ TimeSpan timeZoneOffset;
+ var dateTimeWithZone = GetDateTimeWithZone(
+ settings.AutoSetDaylight,
+ settings.ManualDstAdjustment,
+ settings.ConvertTimeToTimeZone,
+ localDateTime,
+ timeZoneInfo,
+ out timeZoneOffset);
+
+ string timeZoneDisplay = settings.ConvertTimeToTimeZone ?
+ $" ({GetTimeZoneLabel(settings.SelectedTimeZone)}{(timeZoneOffset < TimeSpan.Zero ? "" : "+")}{timeZoneOffset.Hours:00})" :
+ "";
+
+ return GetFormattedTime(
+ dateTimeWithZone,
+ settings.Time24H,
+ settings.ConvertTimeToTimeZone,
+ timeZoneDisplay);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error formatting current time");
+ return "Time Unavailable";
+ }
+ }
+
+ public string UpdateAndGetOutput()
+ {
+ UpdateData();
+ return GetFormattedOutput();
+ }
+
+ public void SaveState()
+ {
+ // Save state if required
+ }
+
+ public void LoadState()
+ {
+ // Load state if required
+ }
+
+ public void Dispose()
+ {
+ Settings.Dispose();
+ }
+
+ private TimeZoneInfo GetTimeZoneInfo(CurrentTimeSettings.TimeZone selectedTimeZone)
+ {
+ string timeZoneId = selectedTimeZone switch
+ {
+ CurrentTimeSettings.TimeZone.UTC => "UTC",
+ CurrentTimeSettings.TimeZone.EST => "Eastern Standard Time",
+ CurrentTimeSettings.TimeZone.CST => "Central Standard Time",
+ CurrentTimeSettings.TimeZone.PST => "Pacific Standard Time",
+ CurrentTimeSettings.TimeZone.CET => "Central European Standard Time",
+ CurrentTimeSettings.TimeZone.AEST => "E. Australia Standard Time",
+ CurrentTimeSettings.TimeZone.GMT => "GMT Standard Time",
+ CurrentTimeSettings.TimeZone.IST => "India Standard Time",
+ CurrentTimeSettings.TimeZone.JST => "Tokyo Standard Time",
+ _ => TimeZoneInfo.Local.Id
+ };
+ return TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
+ }
+
+ private string GetTimeZoneLabel(CurrentTimeSettings.TimeZone timeZone)
+ {
+ return timeZone.ToString();
+ }
+
+ private DateTimeOffset GetDateTimeWithZone(
+ bool autoSetDaylight,
+ bool manualDstAdjustment,
+ bool timeShowTimeZone,
+ DateTimeOffset localDateTime,
+ TimeZoneInfo timeZoneInfo,
+ out TimeSpan timeZoneOffset)
+ {
+ if (!timeShowTimeZone)
+ {
+ // Use local time directly without conversion.
+ timeZoneOffset = TimeZoneInfo.Local.GetUtcOffset(localDateTime);
+ return localDateTime;
+ }
+ else
+ {
+ if (autoSetDaylight)
+ {
+ // Convert time considering daylight saving time.
+ timeZoneOffset = timeZoneInfo.GetUtcOffset(localDateTime);
+ return TimeZoneInfo.ConvertTime(localDateTime, timeZoneInfo);
+ }
+ else
+ {
+ // Convert time without considering daylight saving time adjustments.
+ timeZoneOffset = timeZoneInfo.BaseUtcOffset;
+
+ // Apply manual DST adjustment if specified
+ if (manualDstAdjustment && timeZoneInfo.SupportsDaylightSavingTime && timeZoneInfo.IsDaylightSavingTime(localDateTime))
+ {
+ timeZoneOffset = timeZoneOffset.Add(TimeSpan.FromHours(1));
+ }
+
+ // Adjust dateTimeWithZone to the manually calculated offset
+ var adjustedDateTime = new DateTimeOffset(localDateTime.DateTime, timeZoneOffset);
+ return TimeZoneInfo.ConvertTime(adjustedDateTime, timeZoneInfo);
+ }
+ }
+ }
+
+
+
+
+ private string GetFormattedTime(
+ DateTimeOffset dateTimeWithZone,
+ bool time24H,
+ bool timeShowTimeZone,
+ string timeZoneDisplay)
+ {
+ string timeFormat;
+ if (time24H)
+ {
+ // Use 24-hour format
+ timeFormat = "HH:mm";
+ }
+ else
+ {
+ // Use 12-hour format with AM/PM
+ timeFormat = "hh:mm tt";
+ }
+
+ // If the current culture doesn't support AM/PM as expected, explicitly use a culture that does.
+ CultureInfo culture = new CultureInfo("en-US");
+
+ // Format the dateTimeWithZone using the specified format and culture
+ string formattedTime = dateTimeWithZone.ToString($"{timeFormat}{timeZoneDisplay}", culture);
+
+ return formattedTime;
+ }
+ }
+}
diff --git a/MagicChatboxV2/Services/Modules/WeatherModule.cs b/MagicChatboxV2/Services/Modules/WeatherModule.cs
new file mode 100644
index 0000000..40d7f80
--- /dev/null
+++ b/MagicChatboxV2/Services/Modules/WeatherModule.cs
@@ -0,0 +1,158 @@
+using MagicChatboxV2.UIVM.Models;
+using System;
+using System.Timers;
+using MagicChatboxV2.Services;
+using Timer = System.Timers.Timer;
+
+namespace MagicChatboxV2.Services.Modules
+{
+ public class WeatherModuleSettings : ISettings
+ {
+ public string Location { get; set; } = "New York"; // Default location
+ public int UpdateIntervalMinutes { get; set; } = 1; // Default update interval to 1 minute
+ public string ApiKey { get; set; } = "532ccf9caa1465cf1a7d180c3f1cafdc";
+
+ public WeatherModuleSettings(string apiKey)
+ {
+ if (string.IsNullOrWhiteSpace(apiKey))
+ throw new ArgumentException("API key cannot be null or whitespace.", nameof(apiKey));
+
+ ApiKey = apiKey;
+ }
+
+ public void Dispose()
+ {
+ // If there's anything to dispose, do it here
+ }
+ }
+
+
+
+ public class WeatherModule : IModule
+ {
+ private readonly WeatherService _weatherService;
+ private Timer _updateTimer;
+ private WeatherResponse _currentWeather;
+ private WeatherModuleSettings _settings;
+
+ public string ModuleName => "Local Weather";
+ public ISettings Settings
+ {
+ get => _settings;
+ set
+ {
+ if (value is WeatherModuleSettings settings)
+ {
+ _settings = settings;
+ RestartTimer(); // Restart timer with new settings
+ }
+ }
+ }
+ public bool IsActive { get; set; } = true;
+ public bool IsEnabled { get; set; } = true;
+ public bool IsEnabled_VR { get; set; } = true;
+ public bool IsEnabled_DESKTOP { get; set; } = true;
+ public int ModulePosition { get; set; }
+ public int ModuleMemberGroupNumbers { get; set; }
+ public DateTime LastUpdated { get; private set; }
+
+ public event EventHandler DataUpdated;
+
+ public WeatherModule(WeatherService weatherService, WeatherModuleSettings settings)
+ {
+ _weatherService = weatherService ?? throw new ArgumentNullException(nameof(weatherService));
+ _settings = settings ?? throw new ArgumentNullException(nameof(settings));
+ Initialize();
+ }
+
+ public void Initialize()
+ {
+ InitializeUpdateTimer();
+ }
+
+ private void InitializeUpdateTimer()
+ {
+ _updateTimer = new Timer(_settings.UpdateIntervalMinutes * 60 * 1000);
+ _updateTimer.Elapsed += async (sender, e) => await UpdateDataAsync();
+ _updateTimer.AutoReset = true;
+ _updateTimer.Enabled = IsEnabled; // Enable timer based on IsEnabled property
+ }
+
+ private void RestartTimer()
+ {
+ _updateTimer?.Stop();
+ _updateTimer?.Dispose();
+ InitializeUpdateTimer(); // Create a new timer instance with the updated interval
+ }
+
+ public void UpdateData()
+ {
+ UpdateDataAsync().Wait();
+ }
+
+ public async Task UpdateDataAsync()
+ {
+ if (!IsEnabled || !IsActive) return;
+
+ try
+ {
+ _weatherService.ApiKey = _settings.ApiKey; // Ensure the service uses the current API key
+ _currentWeather = await _weatherService.GetWeatherAsync(_settings.Location);
+ LastUpdated = DateTime.Now;
+ DataUpdated?.Invoke(this, EventArgs.Empty);
+ }
+ catch (Exception ex)
+ {
+ // Log this exception with an error logging framework
+ }
+ }
+
+
+
+
+ public string GetFormattedOutput()
+ {
+ if (_currentWeather == null) return "Weather data is not available.";
+
+ var weatherDesc = _currentWeather.Weather.Count > 0 ? _currentWeather.Weather[0].Description : "N/A";
+ return $"Weather in {_currentWeather.Name}: {_currentWeather.Main.Temp}°C, {weatherDesc}";
+ }
+
+ public string UpdateAndGetOutput()
+ {
+ UpdateDataAsync().Wait();
+ return GetFormattedOutput();
+ }
+
+ public async Task UpdateAndGetOutputAsync()
+ {
+ await UpdateDataAsync();
+ return GetFormattedOutput();
+ }
+
+ public void StartUpdates()
+ {
+ _updateTimer?.Start();
+ }
+
+ public void StopUpdates()
+ {
+ _updateTimer?.Stop();
+ }
+
+ public void SaveState()
+ {
+ // Implement state saving logic, possibly saving to a file or user settings
+ }
+
+ public void LoadState()
+ {
+ // Implement state loading logic, possibly reading from a file or user settings
+ }
+
+ public void Dispose()
+ {
+ _updateTimer?.Dispose();
+ }
+ }
+}
diff --git a/MagicChatboxV2/Services/SystemTrayService.cs b/MagicChatboxV2/Services/SystemTrayService.cs
new file mode 100644
index 0000000..2776bd2
--- /dev/null
+++ b/MagicChatboxV2/Services/SystemTrayService.cs
@@ -0,0 +1,93 @@
+using CommunityToolkit.Mvvm.Input;
+using Hardcodet.Wpf.TaskbarNotification;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media.Imaging;
+using static MagicChatboxV2.App;
+
+namespace MagicChatboxV2.Services
+{
+ public class SystemTrayService
+ {
+ private readonly PrimaryInterfaceFactory _mainWindowFactory;
+ private TaskbarIcon tbIcon;
+
+ public SystemTrayService(PrimaryInterfaceFactory mainWindowFactory)
+ {
+ _mainWindowFactory = mainWindowFactory;
+ }
+
+ public void InitializeTrayIcon()
+ {
+ tbIcon = new TaskbarIcon
+ {
+ Icon = LoadIconFromResource("UIVM/Images/MagicOSC_icon.png"),
+ ToolTipText = "MagicChatboxV2"
+ };
+
+ tbIcon.ContextMenu = new ContextMenu
+ {
+ Items =
+ {
+ new MenuItem { Header = "Start", Command = new RelayCommand(StartApplication) },
+ new MenuItem { Header = "Exit", Command = new RelayCommand(ExitApplication) }
+ }
+ };
+ }
+
+ public void ShowNotification(string title, string message, BalloonIcon icon = BalloonIcon.None)
+ {
+ tbIcon.ShowBalloonTip(title, message, icon);
+ }
+
+
+ private Icon LoadIconFromResource(string resourcePath)
+ {
+ Uri iconUri = new Uri($"pack://application:,,,/{resourcePath}", UriKind.RelativeOrAbsolute);
+ BitmapImage bitmapImage = new BitmapImage(iconUri);
+
+ using (MemoryStream memoryStream = new MemoryStream())
+ {
+ BitmapEncoder encoder = new PngBitmapEncoder();
+ encoder.Frames.Add(BitmapFrame.Create(bitmapImage));
+ encoder.Save(memoryStream);
+ memoryStream.Seek(0, SeekOrigin.Begin);
+
+ // Load the bitmap from stream and create an Icon
+ using (var bitmap = new Bitmap(memoryStream))
+ {
+ return Icon.FromHandle(bitmap.GetHicon());
+ }
+ }
+ }
+
+ public void StartApplication()
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ var mainWindow = _mainWindowFactory();
+ if (!mainWindow.IsVisible)
+ {
+ mainWindow.Show();
+ }
+ else
+ {
+ mainWindow.Activate();
+ }
+ });
+ }
+
+
+ private void ExitApplication()
+ {
+ Application.Current.Shutdown();
+ }
+ }
+}
diff --git a/MagicChatboxV2/Services/VRChatMonitorService.cs b/MagicChatboxV2/Services/VRChatMonitorService.cs
new file mode 100644
index 0000000..ccc6731
--- /dev/null
+++ b/MagicChatboxV2/Services/VRChatMonitorService.cs
@@ -0,0 +1,42 @@
+using System.Diagnostics;
+using System.Timers;
+using Timer = System.Timers.Timer;
+
+namespace MagicChatboxV2.Services
+{
+ public class VRChatMonitorService
+ {
+ private readonly Timer timer;
+ private bool vrChatIsRunningPreviously = false;
+ public bool IsVRChatRunning => !vrChatIsRunningPreviously;
+ public event Action OnVRChatStarted;
+
+ public VRChatMonitorService()
+ {
+ timer = new Timer(10000); // Check every 10 seconds
+ timer.Elapsed += CheckVRChatProcess;
+ }
+
+ public void StartMonitoring()
+ {
+ timer.Start();
+ }
+
+ private void CheckVRChatProcess(object sender, ElapsedEventArgs e)
+ {
+ var vrChatRunning = Process.GetProcessesByName("VRChat").Any();
+
+ if (vrChatRunning && !vrChatIsRunningPreviously)
+ {
+ vrChatIsRunningPreviously = vrChatRunning;
+ OnVRChatStarted?.Invoke(); // Notify about VRChat start
+ }
+ else if (!vrChatRunning)
+ {
+ vrChatIsRunningPreviously = false;
+ // Handle VRChat closure if needed
+ }
+ }
+
+ }
+}
diff --git a/MagicChatboxV2/Services/WeatherService.cs b/MagicChatboxV2/Services/WeatherService.cs
new file mode 100644
index 0000000..796f7b7
--- /dev/null
+++ b/MagicChatboxV2/Services/WeatherService.cs
@@ -0,0 +1,111 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MagicChatboxV2.Services
+{
+ public class WeatherService
+ {
+ private readonly HttpClient _httpClient;
+ private const string BaseUrl = "https://api.openweathermap.org/data/2.5/weather?";
+ private string _apiKey;
+
+ public string ApiKey
+ {
+ get => _apiKey;
+ set
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ throw new ArgumentException("API key cannot be null or whitespace.", nameof(value));
+
+ _apiKey = value;
+ }
+ }
+
+ public WeatherService(HttpClient httpClient, string apiKey)
+ {
+ _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
+ _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey));
+ }
+
+ public async Task GetWeatherAsync(string location)
+ {
+ var requestUrl = $"{BaseUrl}q={location}&appid={_apiKey}&units=metric";
+ return await GetWeatherDataAsync(requestUrl);
+ }
+
+ public async Task GetWeatherByCoordinatesAsync(double latitude, double longitude)
+ {
+ var requestUrl = $"{BaseUrl}lat={latitude}&lon={longitude}&appid={_apiKey}&units=metric";
+ return await GetWeatherDataAsync(requestUrl);
+ }
+
+ private async Task GetWeatherDataAsync(string url)
+ {
+ HttpResponseMessage response = await _httpClient.GetAsync(url);
+ if (response.IsSuccessStatusCode)
+ {
+ var responseContent = await response.Content.ReadAsStringAsync();
+ return JsonConvert.DeserializeObject(responseContent);
+ }
+ else
+ {
+ // Here you can handle various response codes differently if needed
+ throw new HttpRequestException($"Error fetching weather data: {response.ReasonPhrase}");
+ }
+ }
+ }
+
+ public class WeatherResponse
+ {
+ [JsonProperty("weather")]
+ public List Weather { get; set; }
+
+ [JsonProperty("main")]
+ public MainWeatherInfo Main { get; set; }
+
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ public class WeatherDescription
+ {
+ [JsonProperty("id")]
+ public int Id { get; set; }
+
+ [JsonProperty("main")]
+ public string Main { get; set; }
+
+ [JsonProperty("description")]
+ public string Description { get; set; }
+
+ [JsonProperty("icon")]
+ public string Icon { get; set; }
+ }
+
+ public class MainWeatherInfo
+ {
+ [JsonProperty("temp")]
+ public double Temp { get; set; }
+
+ [JsonProperty("feels_like")]
+ public double FeelsLike { get; set; }
+
+ [JsonProperty("temp_min")]
+ public double TempMin { get; set; }
+
+ [JsonProperty("temp_max")]
+ public double TempMax { get; set; }
+
+ [JsonProperty("pressure")]
+ public int Pressure { get; set; }
+
+ [JsonProperty("humidity")]
+ public int Humidity { get; set; }
+ }
+ }
+
+}
diff --git a/MagicChatboxV2/UIVM/Images/MagicOSC_icon.png b/MagicChatboxV2/UIVM/Images/MagicOSC_icon.png
new file mode 100644
index 0000000..7a55c39
Binary files /dev/null and b/MagicChatboxV2/UIVM/Images/MagicOSC_icon.png differ
diff --git a/MagicChatboxV2/UIVM/Models/IModule.cs b/MagicChatboxV2/UIVM/Models/IModule.cs
new file mode 100644
index 0000000..e049987
--- /dev/null
+++ b/MagicChatboxV2/UIVM/Models/IModule.cs
@@ -0,0 +1,28 @@
+namespace MagicChatboxV2.UIVM.Models;
+
+public interface IModule : IDisposable
+{
+ string ModuleName { get; }
+ ISettings Settings { get; set; }
+ bool IsActive { get; set; }
+ bool IsEnabled { get; set; }
+ bool IsEnabled_VR { get; set; }
+ bool IsEnabled_DESKTOP { get; set; }
+ int ModulePosition { get; set; }
+
+ int ModuleMemberGroupNumbers { get; set; }
+ DateTime LastUpdated { get; }
+
+ void Initialize();
+ void StartUpdates();
+ void StopUpdates();
+ void UpdateData();
+ string GetFormattedOutput();
+ string UpdateAndGetOutput();
+ void SaveState();
+ void LoadState();
+ event EventHandler DataUpdated;
+}
+
+
+
diff --git a/MagicChatboxV2/UIVM/Models/ISettings.cs b/MagicChatboxV2/UIVM/Models/ISettings.cs
new file mode 100644
index 0000000..dab418a
--- /dev/null
+++ b/MagicChatboxV2/UIVM/Models/ISettings.cs
@@ -0,0 +1,7 @@
+namespace MagicChatboxV2.UIVM.Models;
+
+ public interface ISettings : IDisposable
+ {
+
+ }
+
diff --git a/MagicChatboxV2/UIVM/Windows/CustomErrorDialog.xaml b/MagicChatboxV2/UIVM/Windows/CustomErrorDialog.xaml
new file mode 100644
index 0000000..422f0be
--- /dev/null
+++ b/MagicChatboxV2/UIVM/Windows/CustomErrorDialog.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/MagicChatboxV2/UIVM/Windows/CustomErrorDialog.xaml.cs b/MagicChatboxV2/UIVM/Windows/CustomErrorDialog.xaml.cs
new file mode 100644
index 0000000..554a253
--- /dev/null
+++ b/MagicChatboxV2/UIVM/Windows/CustomErrorDialog.xaml.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+using System.Windows.Threading;
+
+namespace MagicChatboxV2.UIVM.Windows
+{
+ ///
+ /// Interaction logic for CustomErrorDialog.xaml
+ ///
+ public partial class CustomErrorDialog : Window
+ {
+ private DispatcherTimer autoCloseTimer;
+
+ public CustomErrorDialog(string errorMessage, string stackTrace)
+ {
+ InitializeComponent();
+ txtMainError.Text = errorMessage;
+ txtStackTrace.Text = stackTrace;
+ StartAutoCloseTimer();
+ }
+
+ private void StartAutoCloseTimer()
+ {
+ autoCloseTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; // Adjust time as needed
+ autoCloseTimer.Tick += (sender, args) =>
+ {
+ autoCloseTimer.Stop();
+ this.Close(); // Close dialog after timer
+ };
+ autoCloseTimer.Start();
+ }
+
+ private void BtnContinue_Click(object sender, RoutedEventArgs e)
+ {
+ this.Close(); // Allow user to continue using the application
+ }
+
+ private void BtnExit_Click(object sender, RoutedEventArgs e)
+ {
+ Application.Current.Shutdown(); // Exit application
+ }
+ }
+}
diff --git a/MagicChatboxV2/UIVM/Windows/LoadingWindow.xaml b/MagicChatboxV2/UIVM/Windows/LoadingWindow.xaml
new file mode 100644
index 0000000..beb71f3
--- /dev/null
+++ b/MagicChatboxV2/UIVM/Windows/LoadingWindow.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
diff --git a/MagicChatboxV2/UIVM/Windows/LoadingWindow.xaml.cs b/MagicChatboxV2/UIVM/Windows/LoadingWindow.xaml.cs
new file mode 100644
index 0000000..d1d9123
--- /dev/null
+++ b/MagicChatboxV2/UIVM/Windows/LoadingWindow.xaml.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+
+namespace MagicChatboxV2.UIVM.Windows
+{
+ ///
+ /// Interaction logic for LoadingWindow.xaml
+ ///
+ public partial class LoadingWindow : Window
+ {
+ public LoadingWindow()
+ {
+ InitializeComponent();
+ }
+
+ public void UpdateProgress(double value, string message)
+ {
+ progressBar.Value = value;
+ statusText.Text = message;
+ }
+ }
+
+}
diff --git a/vrcosc-magicchatbox.sln b/vrcosc-magicchatbox.sln
index 8a98bee..55fc719 100644
--- a/vrcosc-magicchatbox.sln
+++ b/vrcosc-magicchatbox.sln
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MagicChatbox", "vrcosc-magicchatbox\MagicChatbox.csproj", "{76FB3E35-94A5-445C-87F2-D75E9F701E5F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MagicChatboxV2", "MagicChatboxV2\MagicChatboxV2.csproj", "{88609669-E9CC-4138-882B-E0576CAB02BF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Beta|Any CPU = Beta|Any CPU
@@ -18,6 +20,12 @@ Global
{76FB3E35-94A5-445C-87F2-D75E9F701E5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76FB3E35-94A5-445C-87F2-D75E9F701E5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76FB3E35-94A5-445C-87F2-D75E9F701E5F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {88609669-E9CC-4138-882B-E0576CAB02BF}.Beta|Any CPU.ActiveCfg = Debug|Any CPU
+ {88609669-E9CC-4138-882B-E0576CAB02BF}.Beta|Any CPU.Build.0 = Debug|Any CPU
+ {88609669-E9CC-4138-882B-E0576CAB02BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {88609669-E9CC-4138-882B-E0576CAB02BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {88609669-E9CC-4138-882B-E0576CAB02BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {88609669-E9CC-4138-882B-E0576CAB02BF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/vrcosc-magicchatbox/Classes/DataAndSecurity/DataController.cs b/vrcosc-magicchatbox/Classes/DataAndSecurity/DataController.cs
index 2a7776b..0a06899 100644
--- a/vrcosc-magicchatbox/Classes/DataAndSecurity/DataController.cs
+++ b/vrcosc-magicchatbox/Classes/DataAndSecurity/DataController.cs
@@ -255,7 +255,8 @@ public static void LoadComponentStats()
{ "OpenAIAccessTokenEncrypted", (typeof(string), "OpenAI") },
{ "OpenAIOrganizationIDEncrypted", (typeof(string), "OpenAI") },
- { "ComponentStatsGPU3DHook", (typeof(bool), "ComponentStats") },
+ { "SelectedGPU", (typeof(string), "ComponentStats") },
+ { "AutoSelectGPU", (typeof(bool), "ComponentStats") },
{ "UseEmojisForTempAndPower", (typeof(bool), "ComponentStats") },
{ "IsTemperatureSwitchEnabled", (typeof(bool), "ComponentStats") },
@@ -341,6 +342,10 @@ public static void LoadComponentStats()
{ "HeartRateTrendIndicatorSampleRate", (typeof(int), "PulsoidConnector") },
{ "HeartRateTitle", (typeof(bool), "PulsoidConnector") },
{ "PulsoidAuthConnected", (typeof(bool), "PulsoidConnector") },
+ { "MagicHeartIconPrefix", (typeof(bool), "PulsoidConnector") },
+ { "CurrentHeartRateTitle", (typeof(string), "PulsoidConnector") },
+ { "EnableHeartRateOfflineCheck ", (typeof(bool), "PulsoidConnector") },
+ { "UnchangedHeartRateLimit ", (typeof(int), "PulsoidConnector") },
{ "LowTemperatureThreshold", (typeof(int), "PulsoidConnector") },
{ "HighTemperatureThreshold", (typeof(int), "PulsoidConnector") },
diff --git a/vrcosc-magicchatbox/Classes/DataAndSecurity/OSCController.cs b/vrcosc-magicchatbox/Classes/DataAndSecurity/OSCController.cs
index 5ac9505..35a6a00 100644
--- a/vrcosc-magicchatbox/Classes/DataAndSecurity/OSCController.cs
+++ b/vrcosc-magicchatbox/Classes/DataAndSecurity/OSCController.cs
@@ -70,38 +70,44 @@ public static void AddHeartRate(List Uncomplete)
{
if (ViewModel.Instance.IntgrHeartRate == true && ViewModel.Instance.HeartRate > 0)
{
- // Pick the correct heart icon
- string heartIcon = ViewModel.Instance.MagicHeartRateIcons || ViewModel.Instance.ShowTemperatureText ? ViewModel.Instance.HeartRateIcon : "💖";
-
- if (ViewModel.Instance.HeartRateTitle)
+ if (ViewModel.Instance.EnableHeartRateOfflineCheck && ViewModel.Instance.PulsoidDeviceOnline || !ViewModel.Instance.EnableHeartRateOfflineCheck)
{
- string hrTitle = "Heart rate" + (ViewModel.Instance.SeperateWithENTERS ? "\v" : ": ");
- string x = ViewModel.Instance.ShowBPMSuffix
- ? ViewModel.Instance.HeartRate + " bpm"
- : (ViewModel.Instance.SeperateWithENTERS ? heartIcon + " " : string.Empty) + ViewModel.Instance.HeartRate;
+ // Always start with the heart icon if MagicHeartRateIcons or ShowTemperatureText is true
+ string displayText = ViewModel.Instance.MagicHeartRateIcons || ViewModel.Instance.ShowTemperatureText || ViewModel.Instance.MagicHeartIconPrefix
+ ? ViewModel.Instance.HeartRateIcon + " "
+ : string.Empty;
- if (ViewModel.Instance.ShowHeartRateTrendIndicator)
+ // Add the heart rate value
+ displayText += ViewModel.Instance.HeartRate.ToString();
+
+ // Optionally append " bpm" suffix if ShowBPMSuffix is true
+ if (ViewModel.Instance.ShowBPMSuffix)
{
- x = x + ViewModel.Instance.HeartRateTrendIndicator;
+ displayText += " bpm";
}
- TryAddToUncomplete(Uncomplete, hrTitle + x, "HeartRate");
- }
- else
- {
- string x = ViewModel.Instance.ShowBPMSuffix
- ? ViewModel.Instance.HeartRate + " bpm"
- : heartIcon + " " + ViewModel.Instance.HeartRate;
+ // Append the HeartRateTrendIndicator if ShowHeartRateTrendIndicator is true
if (ViewModel.Instance.ShowHeartRateTrendIndicator)
{
- x = x + ViewModel.Instance.HeartRateTrendIndicator;
+ displayText += $" {ViewModel.Instance.HeartRateTrendIndicator}";
}
- TryAddToUncomplete(Uncomplete, x, "HeartRate");
+
+ // Add title if HeartRateTitle is true, with a separator based on SeperateWithENTERS
+ if (ViewModel.Instance.HeartRateTitle)
+ {
+ string titleSeparator = ViewModel.Instance.SeperateWithENTERS ? "\v" : ": ";
+ string hrTitle = ViewModel.Instance.CurrentHeartRateTitle + titleSeparator;
+ displayText = hrTitle + displayText;
+ }
+
+ // Finally, add the constructed string to the Uncomplete list with a tag
+ TryAddToUncomplete(Uncomplete, displayText, "HeartRate");
}
}
}
+
public static void AddMediaLink(List Uncomplete)
{
if (ViewModel.Instance.IntgrScanMediaLink)
@@ -460,7 +466,7 @@ public static void BuildOSC()
// Join the list of strings into one string and set the OSCtoSent property in the ViewModel to the final OSC message
if (ViewModel.Instance.SeperateWithENTERS)
{
- Complete_msg = string.Join("\v", Uncomplete);
+ Complete_msg = string.Join("\n", Uncomplete);
}
else
{
diff --git a/vrcosc-magicchatbox/Classes/Modules/ComponentStatsModule.cs b/vrcosc-magicchatbox/Classes/Modules/ComponentStatsModule.cs
index db61a2c..c153072 100644
--- a/vrcosc-magicchatbox/Classes/Modules/ComponentStatsModule.cs
+++ b/vrcosc-magicchatbox/Classes/Modules/ComponentStatsModule.cs
@@ -489,8 +489,7 @@ public void SetStatMaxValueShown(StatsComponentType type, bool state)
public string GenerateStatsDescription()
{
- List lines = new List();
- string currentLine = "";
+ List descriptions = new List();
foreach (var type in StatDisplayOrder)
{
@@ -514,27 +513,24 @@ public string GenerateStatsDescription()
{
gpuTemp = stat.ShowTemperature ? FetchTemperatureStat(gpuHardware, stat) : "";
gpuPower = stat.ShowWattage ? FetchPowerStat(gpuHardware, stat) : "";
- additionalInfo = $"{(!stat.cantShowTemperature ? gpuTemp + " " : "")}{(!stat.cantShowTemperature ? gpuPower : "")}";
+ additionalInfo = $"{(!stat.cantShowTemperature ? gpuTemp + " " : "")}{(!stat.cantShowWattage ? gpuPower : "")}";
}
+ // Combine the component description with additional info if any
+ string fullComponentInfo = $"{componentDescription}{(string.IsNullOrWhiteSpace(additionalInfo) ? "" : $" {additionalInfo}")}".Trim();
- string fullComponentInfo = $"{componentDescription}{(string.IsNullOrWhiteSpace(additionalInfo) ? "" : $"{additionalInfo}")}";
-
-
- lines.Add(currentLine);
- currentLine = fullComponentInfo;
-
+ // Add the full component info to the list of descriptions
+ if (!string.IsNullOrEmpty(fullComponentInfo))
+ {
+ descriptions.Add(fullComponentInfo);
+ }
}
}
- if (!string.IsNullOrWhiteSpace(currentLine))
- {
- lines.Add(currentLine.TrimEnd());
- }
-
ViewModel.Instance.ComponentStatsLastUpdate = DateTime.Now;
- return string.Join(" ¦ ", lines);
+ // Join the descriptions with the separator, ensuring no leading separator when there's only one item
+ return string.Join(" ¦ ", descriptions);
}
@@ -543,6 +539,7 @@ public string GenerateStatsDescription()
+
public static Computer CurrentSystem;
public static void StartMonitoringComponents()
@@ -778,7 +775,7 @@ void UpdateComponentStats(StatsComponentType type, Func fetchStat, Func<
ViewModel.Instance.SetComponentStatMaxValue(type, maxValue);
SetAvailability(type, true);
}
- else if (maxValue != null)
+ else if (maxValue != null && statItem.ShowMaxValue)
{
SetAvailability(type, false);
}
@@ -845,25 +842,61 @@ private static Hardware GetDedicatedGPU()
{
try
{
- foreach (var type in new[] { HardwareType.GpuNvidia, HardwareType.GpuAmd })
+ // Ensure the GPU list in the ViewModel is populated.
+ if (ViewModel.Instance.GPUList == null || !ViewModel.Instance.GPUList.Any())
+ {
+ ViewModel.Instance.GPUList = CurrentSystem.Hardware
+ .Where(h => h.HardwareType == HardwareType.GpuNvidia || h.HardwareType == HardwareType.GpuAmd || h.HardwareType == HardwareType.GpuIntel)
+ .Select(h => h.Name)
+ .ToList();
+ }
+
+ Hardware selectedHardware = null;
+
+ // Use AutoSelectGPU mechanism if SelectedGPU is not set or AutoSelectGPU is true.
+ if (string.IsNullOrEmpty(ViewModel.Instance.SelectedGPU) || ViewModel.Instance.AutoSelectGPU)
{
- var hardware = CurrentSystem.Hardware
- .FirstOrDefault(h => h.HardwareType == type && !h.Name.ToLower().Contains("integrated"))
- as Hardware;
+ // Perform auto-selection of GPU based on predefined criteria.
+ foreach (var type in new[] { HardwareType.GpuNvidia, HardwareType.GpuAmd })
+ {
+ selectedHardware = CurrentSystem.Hardware
+ .FirstOrDefault(h => ViewModel.Instance.GPUList.Contains(h.Name) && h.HardwareType == type && !h.Name.ToLower().Contains("integrated")) as Hardware;
+ if (selectedHardware != null)
+ {
+ ViewModel.Instance.SelectedGPU = selectedHardware.Name; // Update SelectedGPU with the auto-selected GPU name.
+ break; // Break on finding the first dedicated GPU
+ }
+ }
- if (hardware != null) return hardware;
+ // Fallback to integrated GPU if no dedicated GPU is found.
+ if (selectedHardware == null)
+ {
+ selectedHardware = CurrentSystem.Hardware
+ .FirstOrDefault(h => ViewModel.Instance.GPUList.Contains(h.Name) && h.HardwareType == HardwareType.GpuIntel) as Hardware;
+ if (selectedHardware != null)
+ {
+ ViewModel.Instance.SelectedGPU = selectedHardware.Name; // Update SelectedGPU with the auto-selected GPU name.
+ }
+ }
}
- return CurrentSystem.Hardware.FirstOrDefault(h => h.HardwareType == HardwareType.GpuIntel)
- as Hardware;
+ else
+ {
+ // Attempt to use the manually selected GPU if AutoSelectGPU is false and a GPU is selected.
+ selectedHardware = CurrentSystem.Hardware
+ .FirstOrDefault(h => h.Name.Equals(ViewModel.Instance.SelectedGPU, StringComparison.OrdinalIgnoreCase)) as Hardware;
+ }
+
+ return selectedHardware; // Return the selected or auto-selected GPU.
}
catch (Exception ex)
{
Logging.WriteException(ex, MSGBox: false);
- return null;
+ return null; // Return null in case of any exceptions.
}
-
}
+
+
private static string FetchStat(
HardwareType hardwareType,
SensorType sensorType,
@@ -1022,10 +1055,10 @@ private static string FetchGPUStat() =>
FetchStat(HardwareType.GpuNvidia, SensorType.Load, ViewModel.Instance.ComponentStatsGPU3DHook ? "D3D 3D" : "GPU Core", hardwarePredicate: h => h == GetDedicatedGPU(), statsComponentType: StatsComponentType.GPU);
private static string FetchVRAMStat() =>
- FetchStat(HardwareType.GpuNvidia, SensorType.SmallData, "GPU Memory Used", val => val / 1024, h => h == GetDedicatedGPU(), statsComponentType: StatsComponentType.VRAM);
+ FetchStat(HardwareType.GpuNvidia, SensorType.SmallData, ViewModel.Instance.ComponentStatsGPU3DVRAMHook? "D3D Dedicated Memory Used" : "GPU Memory Used", val => val / 1024, h => h == GetDedicatedGPU(), statsComponentType: StatsComponentType.VRAM);
private static string FetchVRAMMaxStat() =>
- FetchStat(HardwareType.GpuNvidia, SensorType.SmallData, "GPU Memory Total", val => val / 1024, h => h == GetDedicatedGPU(), statsComponentType: StatsComponentType.VRAM);
+ FetchStat(HardwareType.GpuNvidia, SensorType.SmallData, ViewModel.Instance.ComponentStatsGPU3DVRAMHook ? "D3D Dedicated Memory Total" : "GPU Memory Total", val => val / 1024, h => h == GetDedicatedGPU(), statsComponentType: StatsComponentType.VRAM);
private static (string UsedMemory, string MaxMemory) FetchRAMStats()
{
diff --git a/vrcosc-magicchatbox/Classes/Modules/PulsoidModule.cs b/vrcosc-magicchatbox/Classes/Modules/PulsoidModule.cs
index 4e7d302..6a68fc3 100644
--- a/vrcosc-magicchatbox/Classes/Modules/PulsoidModule.cs
+++ b/vrcosc-magicchatbox/Classes/Modules/PulsoidModule.cs
@@ -15,6 +15,7 @@
using vrcosc_magicchatbox.Classes.DataAndSecurity;
using Newtonsoft.Json.Linq;
using vrcosc_magicchatbox.DataAndSecurity;
+using System.Windows.Threading;
@@ -26,6 +27,8 @@ public class PulsoidModule
private CancellationTokenSource? _cts;
private readonly Queue> _heartRates = new();
private readonly Queue _heartRateHistory = new();
+ private int _previousHeartRate = -1;
+ private int _unchangedHeartRateCount = 0;
private static double CalculateSlope(Queue values)
{
@@ -126,6 +129,27 @@ private async Task HeartRateMonitoringLoopAsync(CancellationToken cancellationTo
int heartRate = await GetHeartRateViaHttpAsync();
if (heartRate != -1)
{
+ // Check if the heart rate is the same as the previous reading
+ if (heartRate == _previousHeartRate)
+ {
+ _unchangedHeartRateCount++;
+ }
+ else
+ {
+ _unchangedHeartRateCount = 0; // Reset if the heart rate has changed
+ _previousHeartRate = heartRate; // Update previous heart rate
+ }
+
+ // Check if we should perform the offline check
+ if (ViewModel.Instance.EnableHeartRateOfflineCheck && _unchangedHeartRateCount >= ViewModel.Instance.UnchangedHeartRateLimit)
+ {
+
+ ViewModel.Instance.PulsoidDeviceOnline = false; // Set the device as offline
+ }
+ else
+ {
+ ViewModel.Instance.PulsoidDeviceOnline = true; // Otherwise, consider it online
+ }
// Apply the adjustment if ApplyHeartRateAdjustment is true
if (ViewModel.Instance.ApplyHeartRateAdjustment)
{
diff --git a/vrcosc-magicchatbox/MagicChatbox.csproj b/vrcosc-magicchatbox/MagicChatbox.csproj
index 54cfbf3..1a5d534 100644
--- a/vrcosc-magicchatbox/MagicChatbox.csproj
+++ b/vrcosc-magicchatbox/MagicChatbox.csproj
@@ -2,7 +2,7 @@
WinExe
- 0.8.765
+ 0.8.770
net6.0-windows10.0.22000.0
vrcosc_magicchatbox
enable
@@ -169,7 +169,7 @@
-
+
diff --git a/vrcosc-magicchatbox/MainWindow.xaml b/vrcosc-magicchatbox/MainWindow.xaml
index 6470891..16ec177 100644
--- a/vrcosc-magicchatbox/MainWindow.xaml
+++ b/vrcosc-magicchatbox/MainWindow.xaml
@@ -1205,6 +1205,19 @@
Source="/Img/Icons/Cross_v1_ico.png" />
+
+
+
+
+
+
+ Show prefix icon in front of the heart rate
+
+
+
+
+
+
+ Auto change heart rate prefix icons
+
+
+
+
- Auto change heart rate prefix icons
+ IsChecked="{Binding EnableHeartRateOfflineCheck, Mode=TwoWay}"
+ Style="{DynamicResource SettingsCheckbox}"
+ Unchecked="Update_Click">
+ Check for offline heart rate
+
+
+
+
+
+
+
- Show BPM suffix instead of prefix icons 💖
+ Show BPM suffix
@@ -1570,6 +1654,32 @@
Show heart-rate title
+
+
+
+
+
+
+
Show 'My time:' in front of the time integration
+
+
+
+
+
+ Text="{Binding HeartRateLastUpdate, StringFormat='{}{0:T}'}"
+ Visibility="{Binding IntgrHeartRate, Converter={StaticResource InverseBoolToHiddenConverter}, UpdateSourceTrigger=PropertyChanged}" />
+ Text="Something went wrong, check settings"
+ Visibility="{Binding PulsoidAccessError, Converter={StaticResource InverseBoolToHiddenConverter}, UpdateSourceTrigger=PropertyChanged}" />
+
+
+
+
_gpuList;
+ private bool _autoSelectGPU = true; // Default to true for automatically selecting the GPU
+ private string _selectedGPU;
+
+ public List GPUList
+ {
+ get => _gpuList;
+ set
+ {
+ _gpuList = value;
+ NotifyPropertyChanged(nameof(GPUList));
+ }
+ }
+
+ public bool AutoSelectGPU
+ {
+ get => _autoSelectGPU;
+ set
+ {
+ _autoSelectGPU = value;
+ NotifyPropertyChanged(nameof(AutoSelectGPU));
+ }
+ }
+
+ public string SelectedGPU
+ {
+ get => _selectedGPU;
+ set
+ {
+ _selectedGPU = value;
+ NotifyPropertyChanged(nameof(SelectedGPU));
+ }
+ }
+
+
+ private bool _ComponentStatsGPU3DVRAMHook = false;
+ public bool ComponentStatsGPU3DVRAMHook
+ {
+ get { return _ComponentStatsGPU3DVRAMHook; }
+ set
+ {
+ _ComponentStatsGPU3DVRAMHook = value;
+ NotifyPropertyChanged(nameof(ComponentStatsGPU3DVRAMHook));
+ }
+ }
+
+
+ private bool _MagicHeartIconPrefix = true;
+ public bool MagicHeartIconPrefix
+ {
+ get { return _MagicHeartIconPrefix; }
+ set
+ {
+ _MagicHeartIconPrefix = value;
+ NotifyPropertyChanged(nameof(MagicHeartIconPrefix));
+ }
+ }
+
+
#region ICommand's
public ICommand ActivateStatusCommand { get; set; }
@@ -4234,6 +4293,53 @@ public bool IntelliChatAutoLang
}
}
+
+ private bool _PulsoidDeviceOnline = true;
+ public bool PulsoidDeviceOnline
+ {
+ get { return _PulsoidDeviceOnline; }
+ set
+ {
+ _PulsoidDeviceOnline = value;
+ NotifyPropertyChanged(nameof(PulsoidDeviceOnline));
+ }
+ }
+
+
+ private string _CurrentHeartRateTitle = "My heartrate";
+ public string CurrentHeartRateTitle
+ {
+ get { return _CurrentHeartRateTitle; }
+ set
+ {
+ _CurrentHeartRateTitle = value;
+ NotifyPropertyChanged(nameof(CurrentHeartRateTitle));
+ }
+ }
+
+ private int _UnchangedHeartRateLimit = 10;
+ public int UnchangedHeartRateLimit
+ {
+ get { return _UnchangedHeartRateLimit; }
+ set
+ {
+ _UnchangedHeartRateLimit = value;
+ NotifyPropertyChanged(nameof(UnchangedHeartRateLimit));
+ }
+ }
+
+
+ private bool _EnableHeartRateOfflineCheck = true;
+ public bool EnableHeartRateOfflineCheck
+ {
+ get { return _EnableHeartRateOfflineCheck; }
+ set
+ {
+ _EnableHeartRateOfflineCheck = value;
+ NotifyPropertyChanged(nameof(EnableHeartRateOfflineCheck));
+ }
+ }
+
#endregion
#region PropChangedEvent