From bb77c6a4b9d247c1b3bb955e5992b44cb852c3c3 Mon Sep 17 00:00:00 2001
From: SylveonDeko <59923820+SylveonDeko@users.noreply.github.com>
Date: Wed, 30 Oct 2024 09:56:55 -0400
Subject: [PATCH] V3.1 add cross platform support.
---
PresenceClient/PresenceClient-GUI/App.axaml | 50 +-
.../Helpers/TaskExtensions.cs | 18 -
.../Helpers/TrayIconManager.cs | 143 ++-
.../Platform/NetworkUtils.cs | 142 +++
.../Platform/PlatformHelper.cs | 62 ++
.../Platform/SystemUtils.cs | 187 ++++
.../PresenceClient-GUI.csproj | 59 +-
PresenceClient/PresenceClient-GUI/Program.cs | 89 +-
PresenceClient/PresenceClient-GUI/Utils.cs | 2 +-
.../ViewModels/MainWindowViewModel.cs | 839 +++++++++---------
.../Views/MainWindow.axaml.cs | 50 +-
11 files changed, 1127 insertions(+), 514 deletions(-)
delete mode 100644 PresenceClient/PresenceClient-GUI/Helpers/TaskExtensions.cs
create mode 100644 PresenceClient/PresenceClient-GUI/Platform/NetworkUtils.cs
create mode 100644 PresenceClient/PresenceClient-GUI/Platform/PlatformHelper.cs
create mode 100644 PresenceClient/PresenceClient-GUI/Platform/SystemUtils.cs
diff --git a/PresenceClient/PresenceClient-GUI/App.axaml b/PresenceClient/PresenceClient-GUI/App.axaml
index 211cade..f6c9ce2 100644
--- a/PresenceClient/PresenceClient-GUI/App.axaml
+++ b/PresenceClient/PresenceClient-GUI/App.axaml
@@ -1,23 +1,55 @@
+ RequestedThemeVariant="Dark">
+
+
+
-
-
-
+
+
+
+
+
+
+
+
- #FFFFFF
- #000000
- #0078D7
+ #0078D4
+ #005A9E
+ #004275
+ #002642
+ #429CE3
+ #76B9ED
+ #A6D8F7
\ No newline at end of file
diff --git a/PresenceClient/PresenceClient-GUI/Helpers/TaskExtensions.cs b/PresenceClient/PresenceClient-GUI/Helpers/TaskExtensions.cs
deleted file mode 100644
index a1f47ce..0000000
--- a/PresenceClient/PresenceClient-GUI/Helpers/TaskExtensions.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System;
-using System.Threading.Tasks;
-
-namespace PresenceClient.Helpers;
-
-public static class TaskExtensions
-{
- public static void FireAndForget(this Task task)
- {
- task.ContinueWith(t =>
- {
- if (t.IsFaulted)
- {
- Console.WriteLine($"An error occurred: {t.Exception}");
- }
- }, TaskContinuationOptions.OnlyOnFaulted);
- }
-}
\ No newline at end of file
diff --git a/PresenceClient/PresenceClient-GUI/Helpers/TrayIconManager.cs b/PresenceClient/PresenceClient-GUI/Helpers/TrayIconManager.cs
index cc746a5..2a15675 100644
--- a/PresenceClient/PresenceClient-GUI/Helpers/TrayIconManager.cs
+++ b/PresenceClient/PresenceClient-GUI/Helpers/TrayIconManager.cs
@@ -1,77 +1,134 @@
-using Avalonia;
-using Avalonia.Controls;
+using Avalonia.Controls;
using Avalonia.Platform;
using System;
-using Avalonia.Media.Imaging;
using Avalonia.Threading;
using PresenceClient.ViewModels;
-namespace PresenceClient.Helpers;
-
-public class TrayIconManager : IDisposable
+namespace PresenceClient.Helpers
{
- private TrayIcon? trayIcon;
- private readonly MainWindowViewModel _viewModel;
- private NativeMenuItem connectMenuItem;
-
- public TrayIconManager(MainWindowViewModel viewModel)
+ public class TrayIconManager : IDisposable
{
- _viewModel = viewModel;
- InitializeTrayIcon();
- }
+ private readonly MainWindowViewModel viewModel;
+ private TrayIcon? trayIcon;
+ private NativeMenuItem? connectMenuItem;
+ private bool disposed;
+ private bool isEnabled;
- private void InitializeTrayIcon()
- {
- Dispatcher.UIThread.Post(() =>
+ public TrayIconManager(MainWindowViewModel viewModel)
{
- trayIcon = new TrayIcon();
+ this.viewModel = viewModel;
+ }
+
+ public void EnableTrayIcon(bool enable)
+ {
+ if (disposed || enable == isEnabled) return;
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (enable)
+ {
+ InitializeTrayIcon();
+ }
+ else
+ {
+ DisposeTrayIcon();
+ }
+ isEnabled = enable;
+ });
+ }
+ private void InitializeTrayIcon()
+ {
+ if (trayIcon != null || disposed) return;
+
+ trayIcon = new TrayIcon();
var menu = new NativeMenu();
+
var showItem = new NativeMenuItem("Show");
- showItem.Click += (sender, e) => _viewModel.ShowMainWindow();
+ showItem.Click += ShowItem_Click;
menu.Add(showItem);
connectMenuItem = new NativeMenuItem("Connect");
- connectMenuItem.Click += (sender, e) => _viewModel.ToggleConnection();
+ connectMenuItem.Click += ConnectMenuItem_Click;
menu.Add(connectMenuItem);
var exitItem = new NativeMenuItem("Exit");
- exitItem.Click += (sender, e) => _viewModel.ExitApplication();
+ exitItem.Click += ExitItem_Click;
menu.Add(exitItem);
trayIcon.Menu = menu;
trayIcon.Clicked += TrayIcon_Clicked;
- UpdateIcon(false); // Start with disconnected icon
+ UpdateIcon(false);
trayIcon.IsVisible = true;
- });
- }
+ }
- private void TrayIcon_Clicked(object? sender, EventArgs e)
- {
- _viewModel.ShowMainWindow();
- }
+ private void ShowItem_Click(object? sender, EventArgs e)
+ {
+ viewModel.ShowMainWindow();
+ }
- public void UpdateIcon(bool isConnected)
- {
- Dispatcher.UIThread.Post(() =>
+ private void ConnectMenuItem_Click(object? sender, EventArgs e)
+ {
+ viewModel.ToggleConnection();
+ }
+
+ private void ExitItem_Click(object? sender, EventArgs e)
+ {
+ viewModel.ExitApplication();
+ }
+
+ private void TrayIcon_Clicked(object? sender, EventArgs e)
+ {
+ viewModel.ShowMainWindow();
+ }
+
+ public void UpdateIcon(bool isConnected)
+ {
+ if (disposed) return;
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (trayIcon == null || connectMenuItem == null) return;
+
+ var iconName = isConnected ? "Connected.ico" : "Disconnected.ico";
+ using var assets = AssetLoader.Open(new Uri($"avares://PresenceClient-GUI/Assets/{iconName}"));
+
+ trayIcon.Icon = new WindowIcon(assets);
+ trayIcon.ToolTipText = $"PresenceClient ({(isConnected ? "Connected" : "Disconnected")})";
+ connectMenuItem.Header = isConnected ? "Disconnect" : "Connect";
+ });
+ }
+
+ private void DisposeTrayIcon()
{
if (trayIcon == null) return;
- var iconName = isConnected ? "Connected.ico" : "Disconnected.ico";
- var assets = AssetLoader.Open(new Uri($"avares://PresenceClient-GUI/Assets/{iconName}"));
- trayIcon.Icon = new WindowIcon(assets);
- trayIcon.ToolTipText = $"PresenceClient ({(isConnected ? "Connected" : "Disconnected")})";
+ trayIcon.IsVisible = false;
+ trayIcon.Clicked -= TrayIcon_Clicked;
- connectMenuItem.Header = isConnected ? "Disconnect" : "Connect";
- });
- }
+ if (trayIcon.Menu != null)
+ {
+ foreach (var item in trayIcon.Menu.Items)
+ {
+ if (item is not NativeMenuItem menuItem) continue;
+ menuItem.Click -= ShowItem_Click;
+ menuItem.Click -= ConnectMenuItem_Click;
+ menuItem.Click -= ExitItem_Click;
+ }
+ }
- public void Dispose()
- {
- Dispatcher.UIThread.Post(() =>
+ trayIcon.Dispose();
+ trayIcon = null;
+ connectMenuItem = null;
+ }
+
+ public void Dispose()
{
- trayIcon?.Dispose();
- });
+ if (disposed) return;
+
+ Dispatcher.UIThread.Post(DisposeTrayIcon);
+ disposed = true;
+ }
}
}
\ No newline at end of file
diff --git a/PresenceClient/PresenceClient-GUI/Platform/NetworkUtils.cs b/PresenceClient/PresenceClient-GUI/Platform/NetworkUtils.cs
new file mode 100644
index 0000000..0aa593e
--- /dev/null
+++ b/PresenceClient/PresenceClient-GUI/Platform/NetworkUtils.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Diagnostics;
+using System.Text.RegularExpressions;
+
+namespace PresenceClient.Platform
+{
+ public static class NetworkUtils
+ {
+ public static string GetMacByIp(string ip)
+ {
+ if (string.IsNullOrEmpty(ip))
+ return string.Empty;
+
+ try
+ {
+ if (PlatformHelper.IsWindows)
+ return GetWindowsMacByIp(ip);
+ else
+ return GetUnixMacByIp(ip);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error getting MAC address: {ex.Message}");
+ return string.Empty;
+ }
+ }
+
+ public static string GetIpByMac(string mac)
+ {
+ if (string.IsNullOrEmpty(mac))
+ return string.Empty;
+
+ try
+ {
+ if (PlatformHelper.IsWindows)
+ return GetWindowsIpByMac(mac);
+ else
+ return GetUnixIpByMac(mac);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error getting IP address: {ex.Message}");
+ return string.Empty;
+ }
+ }
+
+ private static string GetWindowsMacByIp(string ip)
+ {
+ var output = RunCommand("arp", $"-a {ip}");
+ var match = Regex.Match(output, @"(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}");
+ return match.Success ? match.Value.Replace("-", ":").ToLower() : string.Empty;
+ }
+
+ private static string GetWindowsIpByMac(string mac)
+ {
+ var output = RunCommand("arp", "-a");
+ mac = mac.Replace(":", "-").ToUpper();
+ var match = Regex.Match(output, $@"([0-9.]{{7,15}})\s+{mac}");
+ return match.Success ? match.Groups[1].Value : string.Empty;
+ }
+
+ private static string GetUnixMacByIp(string ip)
+ {
+ string command, arguments;
+ if (PlatformHelper.IsMacOS)
+ {
+ command = "arp";
+ arguments = $"-n {ip}";
+ }
+ else // Linux
+ {
+ command = "ip";
+ arguments = $"neigh show {ip}";
+ }
+
+ var output = RunCommand(command, arguments);
+ var match = Regex.Match(output, @"(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}");
+ return match.Success ? match.Value.ToLower() : string.Empty;
+ }
+
+ private static string GetUnixIpByMac(string mac)
+ {
+ string command, arguments;
+ if (PlatformHelper.IsMacOS)
+ {
+ command = "arp";
+ arguments = "-an";
+ }
+ else // Linux
+ {
+ command = "ip";
+ arguments = "neigh show";
+ }
+
+ var output = RunCommand(command, arguments);
+ mac = mac.ToLower();
+ var match = Regex.Match(output, $@"([0-9.]{{7,15}}).*{mac}");
+ return match.Success ? match.Groups[1].Value : string.Empty;
+ }
+
+ private static string RunCommand(string command, string arguments)
+ {
+ using var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = command,
+ Arguments = arguments,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ }
+ };
+
+ try
+ {
+ process.Start();
+ var output = process.StandardOutput.ReadToEnd();
+ process.WaitForExit();
+ return output;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error running command {command}: {ex.Message}");
+ return string.Empty;
+ }
+ }
+
+ public static bool IsValidMacAddress(string mac)
+ {
+ return !string.IsNullOrEmpty(mac) &&
+ Regex.IsMatch(mac, "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$");
+ }
+
+ public static bool IsValidIpAddress(string ip)
+ {
+ return !string.IsNullOrEmpty(ip) &&
+ Regex.IsMatch(ip, @"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
+ }
+ }
+}
\ No newline at end of file
diff --git a/PresenceClient/PresenceClient-GUI/Platform/PlatformHelper.cs b/PresenceClient/PresenceClient-GUI/Platform/PlatformHelper.cs
new file mode 100644
index 0000000..163cf9f
--- /dev/null
+++ b/PresenceClient/PresenceClient-GUI/Platform/PlatformHelper.cs
@@ -0,0 +1,62 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace PresenceClient.Platform;
+
+public static class PlatformHelper
+{
+ public static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ public static bool IsMacOS => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
+ public static bool IsLinux => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+
+ public static string GetConfigDirectory()
+ {
+ if (IsWindows)
+ return AppContext.BaseDirectory;
+ else if (IsMacOS)
+ return Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ "Library/Application Support/PresenceClient");
+ else // Linux
+ return Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ ".config/presenceclient");
+ }
+
+ public static string GetConfigPath()
+ {
+ EnsureConfigDirectory();
+ return Path.Combine(GetConfigDirectory(), "Config.json");
+ }
+
+ public static string GetResourcePath(string filename)
+ {
+ if (IsWindows)
+ return Path.Combine(AppContext.BaseDirectory, "Assets", filename);
+ else if (IsMacOS)
+ return Path.Combine(AppContext.BaseDirectory, "Contents", "Resources", filename);
+ else // Linux
+ return Path.Combine("/usr/share/presenceclient", filename);
+ }
+
+ public static void EnsureConfigDirectory()
+ {
+ var configDir = GetConfigDirectory();
+ if (!Directory.Exists(configDir))
+ {
+ Directory.CreateDirectory(configDir);
+ }
+ }
+
+ public static bool CanUseTrayIcon()
+ {
+ if (IsWindows)
+ return true;
+ if (IsMacOS)
+ return true;
+ if (IsLinux)
+ return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP"));
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/PresenceClient/PresenceClient-GUI/Platform/SystemUtils.cs b/PresenceClient/PresenceClient-GUI/Platform/SystemUtils.cs
new file mode 100644
index 0000000..3a4d7ae
--- /dev/null
+++ b/PresenceClient/PresenceClient-GUI/Platform/SystemUtils.cs
@@ -0,0 +1,187 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PresenceClient.Platform
+{
+ public static class SystemUtils
+ {
+ public static async Task SetAutoStartAsync(bool enable)
+ {
+ try
+ {
+ if (PlatformHelper.IsWindows)
+ {
+ await SetWindowsAutoStartAsync(enable);
+ }
+ else if (PlatformHelper.IsMacOS)
+ {
+ await SetMacOSAutoStartAsync(enable);
+ }
+ else if (PlatformHelper.IsLinux)
+ {
+ await SetLinuxAutoStartAsync(enable);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error setting auto start: {ex.Message}");
+ }
+ }
+
+ private static async Task SetWindowsAutoStartAsync(bool enable)
+ {
+ var startupFolder = Environment.GetFolderPath(Environment.SpecialFolder.Startup);
+ var shortcutPath = Path.Combine(startupFolder, "PresenceClient.lnk");
+ var exePath = Process.GetCurrentProcess().MainModule?.FileName ??
+ Path.Combine(AppContext.BaseDirectory, "PresenceClient.exe");
+
+ if (enable)
+ {
+ var shellLink = (IShellLink)new ShellLink();
+ shellLink.SetPath(exePath);
+ shellLink.SetWorkingDirectory(Path.GetDirectoryName(exePath));
+
+ var persistFile = (IPersistFile)shellLink;
+ persistFile.Save(shortcutPath, false);
+
+ Marshal.ReleaseComObject(shellLink);
+ Marshal.ReleaseComObject(persistFile);
+ }
+ else
+ {
+ if (File.Exists(shortcutPath))
+ {
+ File.Delete(shortcutPath);
+ }
+ }
+ }
+
+ [ComImport]
+ [Guid("00021401-0000-0000-C000-000000000046")]
+ private class ShellLink { }
+
+ [ComImport]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("000214F9-0000-0000-C000-000000000046")]
+ private interface IShellLink
+ {
+ void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, out IntPtr pfd, int fFlags);
+ void GetIDList(out IntPtr ppidl);
+ void SetIDList(IntPtr pidl);
+ void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
+ void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
+ void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
+ void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
+ void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
+ void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
+ void GetHotkey(out short pwHotkey);
+ void SetHotkey(short wHotkey);
+ void GetShowCmd(out int piShowCmd);
+ void SetShowCmd(int iShowCmd);
+ void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
+ void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
+ void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
+ void Resolve(IntPtr hwnd, int fFlags);
+ void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
+ }
+
+ [ComImport]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("0000010B-0000-0000-C000-000000000046")]
+ internal interface IPersistFile
+ {
+ void GetCurFile([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile);
+ void IsDirty();
+ void Load([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName, uint dwMode);
+ void Save([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName, [In, MarshalAs(UnmanagedType.Bool)] bool fRemember);
+ void SaveCompleted([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName);
+ }
+
+ private static async Task SetMacOSAutoStartAsync(bool enable)
+ {
+ var plistPath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ "Library/LaunchAgents/com.presenceclient.app.plist");
+
+ if (enable)
+ {
+ var plistContent = $@"
+
+
+
+ Label
+ com.presenceclient.app
+ ProgramArguments
+
+ {Process.GetCurrentProcess().MainModule?.FileName}
+
+ RunAtLoad
+
+
+";
+ await File.WriteAllTextAsync(plistPath, plistContent);
+ await RunCommandAsync("launchctl", $"load {plistPath}");
+ }
+ else
+ {
+ if (File.Exists(plistPath))
+ {
+ await RunCommandAsync("launchctl", $"unload {plistPath}");
+ File.Delete(plistPath);
+ }
+ }
+ }
+
+ private static async Task SetLinuxAutoStartAsync(bool enable)
+ {
+ var autostartDir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ ".config/autostart");
+ var desktopFile = Path.Combine(autostartDir, "presenceclient.desktop");
+
+ if (enable)
+ {
+ Directory.CreateDirectory(autostartDir);
+ var desktopEntry = $@"[Desktop Entry]
+Type=Application
+Name=PresenceClient
+Exec={Process.GetCurrentProcess().MainModule?.FileName}
+Hidden=false
+NoDisplay=false
+X-GNOME-Autostart-enabled=true";
+ await File.WriteAllTextAsync(desktopFile, desktopEntry);
+ }
+ else
+ {
+ if (File.Exists(desktopFile))
+ {
+ File.Delete(desktopFile);
+ }
+ }
+ }
+
+ private static async Task RunCommandAsync(string command, string arguments)
+ {
+ using var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = command,
+ Arguments = arguments,
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ }
+ };
+
+ process.Start();
+ var output = await process.StandardOutput.ReadToEndAsync();
+ await process.WaitForExitAsync();
+ return output;
+ }
+ }
+}
\ No newline at end of file
diff --git a/PresenceClient/PresenceClient-GUI/PresenceClient-GUI.csproj b/PresenceClient/PresenceClient-GUI/PresenceClient-GUI.csproj
index 8609dfb..460ed02 100644
--- a/PresenceClient/PresenceClient-GUI/PresenceClient-GUI.csproj
+++ b/PresenceClient/PresenceClient-GUI/PresenceClient-GUI.csproj
@@ -6,32 +6,57 @@
true
app.manifest
true
- Assets\Icon.ico
true
+ full
+ true
+
+
+
+ DEBUG;TRACE
-
-
-
-
-
-
-
-
-
-
-
-
+
+ $(DefineConstants);WINDOWS
+ Assets\Icon.ico
+
+
+
+ $(DefineConstants);OSX
+ PresenceClient
+ com.presenceclient.app
+ 1.0.0
+ 1.0.0
+ APPL
+ ????
+ PresenceClient
+ Icon.icns
+ true
+ false
+
+
+
+ $(DefineConstants);LINUX
+
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+
\ No newline at end of file
diff --git a/PresenceClient/PresenceClient-GUI/Program.cs b/PresenceClient/PresenceClient-GUI/Program.cs
index 93b3ab3..9ff22a8 100644
--- a/PresenceClient/PresenceClient-GUI/Program.cs
+++ b/PresenceClient/PresenceClient-GUI/Program.cs
@@ -1,27 +1,84 @@
using System;
using Avalonia;
using Avalonia.ReactiveUI;
+using PresenceClient.Platform;
-namespace PresenceClient;
-
-internal class Program
+namespace PresenceClient
{
- [STAThread]
- public static void Main(string[] args)
+ internal class Program
{
- BuildAvaloniaApp()
- .StartWithClassicDesktopLifetime(args);
- }
+ [STAThread]
+ public static void Main(string[] args)
+ {
+ try
+ {
+ BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Application crashed: {ex}");
+ throw;
+ }
+ }
+
+ public static AppBuilder BuildAvaloniaApp()
+ {
+ var builder = AppBuilder.Configure()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .UseReactiveUI();
+
+ if (PlatformHelper.IsLinux)
+ {
+ builder.With(new X11PlatformOptions
+ {
+ EnableMultiTouch = true,
+ UseDBusMenu = true,
+ UseDBusFilePicker = true,
+ RenderingMode =
+ [
+ X11RenderingMode.Glx,
+ X11RenderingMode.Egl,
+ X11RenderingMode.Software
+ ]
+ });
+ }
+ else if (PlatformHelper.IsWindows)
+ {
+ builder.With(new Win32PlatformOptions
+ {
+ RenderingMode =
+ [
+ Win32RenderingMode.AngleEgl,
+ Win32RenderingMode.Wgl,
+ Win32RenderingMode.Software
+ ],
+ CompositionMode =
+ [
+ Win32CompositionMode.WinUIComposition,
+ Win32CompositionMode.DirectComposition,
+ Win32CompositionMode.RedirectionSurface
+ ],
+ DpiAwareness = Win32DpiAwareness.PerMonitorDpiAware
+ });
+ }
+ else if (PlatformHelper.IsMacOS)
+ {
+ builder.With(new MacOSPlatformOptions
+ {
+ ShowInDock = true,
+ DisableDefaultApplicationMenuItems = false,
+ DisableNativeMenus = false,
+ DisableSetProcessName = false,
+ });
+ }
- private static AppBuilder BuildAvaloniaApp()
- {
- return AppBuilder.Configure()
- .UsePlatformDetect()
- .WithInterFont()
#if DEBUG
- .LogToTrace()
+ builder.LogToTrace();
#endif
- .UseReactiveUI()
- .UseSkia();
+
+ return builder;
+ }
}
}
\ No newline at end of file
diff --git a/PresenceClient/PresenceClient-GUI/Utils.cs b/PresenceClient/PresenceClient-GUI/Utils.cs
index 121ad95..548729d 100644
--- a/PresenceClient/PresenceClient-GUI/Utils.cs
+++ b/PresenceClient/PresenceClient-GUI/Utils.cs
@@ -5,7 +5,7 @@
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
-namespace PresenceClient_GUI;
+namespace PresenceClient;
public static partial class Utils
{
diff --git a/PresenceClient/PresenceClient-GUI/ViewModels/MainWindowViewModel.cs b/PresenceClient/PresenceClient-GUI/ViewModels/MainWindowViewModel.cs
index 2cb9ccf..05c9f51 100644
--- a/PresenceClient/PresenceClient-GUI/ViewModels/MainWindowViewModel.cs
+++ b/PresenceClient/PresenceClient-GUI/ViewModels/MainWindowViewModel.cs
@@ -7,529 +7,578 @@
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
-using Avalonia.Media;
-using Avalonia.Media.Transformation;
using Avalonia.Threading;
using DiscordRPC;
-using Newtonsoft.Json;
-using PresenceClient_GUI;
using PresenceClient.Helpers;
+using PresenceClient.Platform;
using PresenceClient.Views;
using PresenceCommon.Types;
+using System.Text.Json;
+using System.Text.Json.Serialization;
using ReactiveUI;
-namespace PresenceClient.ViewModels;
-
-public class MainWindowViewModel : ReactiveObject, IDisposable
+namespace PresenceClient.ViewModels
{
- private static TrayIconManager? trayIconManager;
- private bool autoConvertIpToMac;
- private string bigImageKey = "";
- private string bigImageText = "";
-
- private CancellationTokenSource? cancellationTokenSource;
- private Socket? client;
- private string clientId = "";
- private bool displayHomeMenu = true;
- private bool hasSeenMacPrompt;
- private string ipAddress = "";
- private bool isConnected;
- private string lastProgramName = string.Empty;
- private bool manualUpdate;
- private bool minimizeToTray;
- private IPAddress? resolvedIpAddress;
- private DiscordRpcClient? rpc;
- private bool showTimeLapsed = true;
- private string smallImageKey = "";
- private string stateText = "";
- private string status = "";
- private Timestamps? time;
-
- public MainWindowViewModel()
+ public class MainWindowViewModel : ReactiveObject, IDisposable
{
- LoadConfig();
- var canConnect = this.WhenAnyValue(x => x.IsConnected).Select(connected => !connected);
- ConnectCommand = ReactiveCommand.CreateFromTask(ConnectAsync, canConnect);
-
- var canDisconnect = this.WhenAnyValue(x => x.IsConnected);
- DisconnectCommand = ReactiveCommand.Create(Disconnect, canDisconnect);
+ private static TrayIconManager? trayIconManager;
+ private bool autoConvertIpToMac;
+ private string bigImageKey = "";
+ private string bigImageText = "";
+ private CancellationTokenSource? cancellationTokenSource;
+ private Socket? client;
+ private string clientId = "";
+ private bool displayHomeMenu = true;
+ private bool hasSeenMacPrompt;
+ private string ipAddress = "";
+ private bool isConnected;
+ private string lastProgramName = string.Empty;
+ private bool manualUpdate;
+ private bool minimizeToTray;
+ private IPAddress? resolvedIpAddress;
+ private DiscordRpcClient? rpc;
+ private bool showTimeLapsed = true;
+ private string smallImageKey = "";
+ private string stateText = "";
+ private string status = "";
+ private Timestamps? time;
+ private UserControl currentPage;
+ private readonly string _configPath;
+
+ public MainWindowViewModel()
+ {
+ _configPath = PlatformHelper.GetConfigPath();
+ currentPage = new MainPage();
+
+ if (PlatformHelper.CanUseTrayIcon())
+ {
+ trayIconManager ??= new TrayIconManager(this);
+ }
- ShowMainCommand = ReactiveCommand.Create(ShowMain);
- ShowSettingsCommand = ReactiveCommand.Create(ShowSettings);
+ LoadConfig();
- trayIconManager ??= new TrayIconManager(this);
- }
+ var canConnect = this.WhenAnyValue(x => x.IsConnected).Select(connected => !connected);
+ ConnectCommand = ReactiveCommand.CreateFromTask(ConnectAsync, canConnect);
- public ReactiveCommand ConnectCommand { get; }
- public ReactiveCommand DisconnectCommand { get; }
+ var canDisconnect = this.WhenAnyValue(x => x.IsConnected);
+ DisconnectCommand = ReactiveCommand.Create(Disconnect, canDisconnect);
- private UserControl currentPage = new MainPage();
- public UserControl CurrentPage
- {
- set => this.RaiseAndSetIfChanged(ref currentPage, value);
- }
+ ReactiveCommand.Create(ShowMain);
+ ReactiveCommand.Create(ShowSettings);
+ }
- public ReactiveCommand ShowMainCommand;
- public ReactiveCommand ShowSettingsCommand;
+ public ReactiveCommand ConnectCommand { get; }
+ public ReactiveCommand DisconnectCommand { get; }
- public string IpAddress
- {
- get
- {
- return ipAddress;
- }
- set
+ public UserControl CurrentPage
{
- this.RaiseAndSetIfChanged(ref ipAddress, value);
- SaveConfig();
+ get => currentPage;
+ set => this.RaiseAndSetIfChanged(ref currentPage, value);
}
- }
- public string ClientId
- {
- get
- {
- return clientId;
- }
- set
+ public string IpAddress
{
- this.RaiseAndSetIfChanged(ref clientId, value);
- SaveConfig();
+ get => ipAddress;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref ipAddress, value);
+ SaveConfig();
+ }
}
- }
- public string BigImageKey
- {
- get
+ public string ClientId
{
- return bigImageKey;
- }
- set
- {
- this.RaiseAndSetIfChanged(ref bigImageKey, value);
- SaveConfig();
+ get => clientId;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref clientId, value);
+ SaveConfig();
+ }
}
- }
- public string BigImageText
- {
- get
- {
- return bigImageText;
- }
- set
+ public string BigImageKey
{
- this.RaiseAndSetIfChanged(ref bigImageText, value);
- SaveConfig();
+ get => bigImageKey;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref bigImageKey, value);
+ SaveConfig();
+ }
}
- }
- public string SmallImageKey
- {
- get
- {
- return smallImageKey;
- }
- set
+ public string BigImageText
{
- this.RaiseAndSetIfChanged(ref smallImageKey, value);
- SaveConfig();
+ get => bigImageText;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref bigImageText, value);
+ SaveConfig();
+ }
}
- }
- public string StateText
- {
- get
+ public string SmallImageKey
{
- return stateText;
- }
- set
- {
- this.RaiseAndSetIfChanged(ref stateText, value);
- SaveConfig();
+ get => smallImageKey;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref smallImageKey, value);
+ SaveConfig();
+ }
}
- }
- public bool ShowTimeLapsed
- {
- get
- {
- return showTimeLapsed;
- }
- set
+ public string StateText
{
- this.RaiseAndSetIfChanged(ref showTimeLapsed, value);
- SaveConfig();
+ get => stateText;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref stateText, value);
+ SaveConfig();
+ }
}
- }
- public bool MinimizeToTray
- {
- get
+ public bool ShowTimeLapsed
{
- return minimizeToTray;
- }
- set
- {
- this.RaiseAndSetIfChanged(ref minimizeToTray, value);
- SaveConfig();
+ get => showTimeLapsed;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref showTimeLapsed, value);
+ SaveConfig();
+ }
}
- }
- public bool DisplayHomeMenu
- {
- get
+ public bool MinimizeToTray
{
- return displayHomeMenu;
- }
- set
- {
- this.RaiseAndSetIfChanged(ref displayHomeMenu, value);
- SaveConfig();
+ get => minimizeToTray;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref minimizeToTray, value);
+ SaveConfig();
+ }
}
- }
- public bool AutoConvertIpToMac
- {
- get
- {
- return autoConvertIpToMac;
- }
- set
+ public bool DisplayHomeMenu
{
- this.RaiseAndSetIfChanged(ref autoConvertIpToMac, value);
- SaveConfig();
+ get => displayHomeMenu;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref displayHomeMenu, value);
+ SaveConfig();
+ }
}
- }
- public string Status
- {
- get
+ public bool AutoConvertIpToMac
{
- return status;
- }
- set
- {
- this.RaiseAndSetIfChanged(ref status, value);
- SaveConfig();
+ get => autoConvertIpToMac;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref autoConvertIpToMac, value);
+ SaveConfig();
+ }
}
- }
- public bool IsConnected
- {
- get
+ public string Status
{
- return isConnected;
+ get => status;
+ set => this.RaiseAndSetIfChanged(ref status, value);
}
- set
+
+ public bool IsConnected
{
- this.RaiseAndSetIfChanged(ref isConnected, value);
- SaveConfig();
+ get => isConnected;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref isConnected, value);
+ SaveConfig();
+ }
}
- }
-
- public event EventHandler? ShowMainWindowRequested;
- public void ShowMainWindow()
- {
- ShowMainWindowRequested?.Invoke(this, EventArgs.Empty);
- }
+ public event EventHandler? ShowMainWindowRequested;
- public void ToggleConnection()
- {
- if (IsConnected)
+ public void ShowMainWindow()
{
- Disconnect();
+ ShowMainWindowRequested?.Invoke(this, EventArgs.Empty);
}
- else
+
+ public void ToggleConnection()
{
- ConnectAsync().FireAndForget();
+ if (IsConnected)
+ {
+ Disconnect();
+ }
+ else
+ {
+ _ = ConnectAsync();
+ }
}
- }
- private async Task UpdateConnectionStatusAsync(bool isConnected)
- {
- await Dispatcher.UIThread.InvokeAsync(() =>
+ private async Task UpdateConnectionStatusAsync(bool isConnectedSecond)
{
- IsConnected = isConnected;
- trayIconManager?.UpdateIcon(isConnected);
- });
- }
-
- public void ExitApplication()
- {
- SaveConfig();
- Disconnect();
- Environment.Exit(0);
- }
-
- public void Dispose()
- {
- if (trayIconManager == null) return;
- trayIconManager.Dispose();
- trayIconManager = null;
- }
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ IsConnected = isConnectedSecond;
+ trayIconManager?.UpdateIcon(isConnectedSecond);
+ });
+ }
- private void ShowMain()
- {
- Dispatcher.UIThread.Post(() =>
+ public void ExitApplication()
{
- CurrentPage = new MainPage { DataContext = this };
- });
- }
+ Disconnect();
+ Environment.Exit(0);
+ }
- private void ShowSettings()
- {
- Dispatcher.UIThread.Post(() =>
- {
- CurrentPage = new SettingsPage { DataContext = this };
- });
- }
- private async Task ConnectAsync()
- {
- if (isConnected)
+ public void Dispose()
{
Disconnect();
- return;
+ if (trayIconManager != null)
+ {
+ trayIconManager.Dispose();
+ trayIconManager = null;
+ }
}
- if (string.IsNullOrWhiteSpace(clientId))
+ private void ShowMain()
{
- Status = "Client ID cannot be empty";
- return;
+ Dispatcher.UIThread.Post(() =>
+ {
+ CurrentPage = new MainPage
+ {
+ DataContext = this
+ };
+ });
}
- // Check and see if we have an IP
- if (IPAddress.TryParse(ipAddress, out resolvedIpAddress))
+ private void ShowSettings()
{
- if (!hasSeenMacPrompt)
+ Dispatcher.UIThread.Post(() =>
{
- hasSeenMacPrompt = true;
- autoConvertIpToMac = true;
- await IpToMacAsync();
- }
- else if (autoConvertIpToMac)
- {
- await IpToMacAsync();
- }
+ CurrentPage = new SettingsPage
+ {
+ DataContext = this
+ };
+ });
}
- else
+
+ private async Task ConnectAsync()
{
- // If in this block, means we don't have a valid IP.
- // Check and see if it's a MAC Address
- try
+ if (isConnected)
{
- resolvedIpAddress = IPAddress.Parse(Utils.GetIpByMac(ipAddress));
+ Disconnect();
+ return;
}
- catch (FormatException)
+
+ if (string.IsNullOrWhiteSpace(clientId))
{
- Status = "Invalid IP or MAC Address";
+ Status = "Client ID cannot be empty";
return;
}
- }
- cancellationTokenSource = new CancellationTokenSource();
- isConnected = true;
+ try
+ {
+ if (IPAddress.TryParse(ipAddress, out resolvedIpAddress))
+ {
+ if (!hasSeenMacPrompt)
+ {
+ hasSeenMacPrompt = true;
+ autoConvertIpToMac = true;
+ await IpToMacAsync();
+ }
+ else if (autoConvertIpToMac)
+ {
+ await IpToMacAsync();
+ }
+ }
+ else
+ {
+ var resolvedIp = NetworkUtils.GetIpByMac(ipAddress);
+ if (string.IsNullOrEmpty(resolvedIp))
+ {
+ Status = "Invalid IP or MAC Address";
+ return;
+ }
- try
- {
- await Task.Run(() => TryConnect(cancellationTokenSource.Token));
- }
- catch (OperationCanceledException)
- {
- Status = "Connection was cancelled";
- }
- catch (Exception ex)
- {
- Status = $"Connection error: {ex.Message}";
- isConnected = false;
- }
- }
+ resolvedIpAddress = IPAddress.Parse(resolvedIp);
+ }
- private void Disconnect()
- {
- cancellationTokenSource?.Cancel();
- rpc?.Dispose();
- rpc = null;
- client?.Close();
- client = null;
- isConnected = false;
- Status = "Disconnected";
- lastProgramName = string.Empty;
- time = null;
- UpdateConnectionStatusAsync(false).FireAndForget();
- }
+ cancellationTokenSource = new CancellationTokenSource();
+ isConnected = true;
- private async Task TryConnect(CancellationToken cancellationToken)
- {
- rpc = new DiscordRpcClient(clientId);
- rpc.Initialize();
+ await Task.Run(() => TryConnect(cancellationTokenSource.Token));
+ }
+ catch (OperationCanceledException)
+ {
+ Status = "Connection was cancelled";
+ }
+ catch (Exception ex)
+ {
+ Status = $"Connection error: {ex.Message}";
+ isConnected = false;
+ }
+ }
- while (!cancellationToken.IsCancellationRequested)
+ private void Disconnect()
{
try
{
- client = new Socket(SocketType.Stream, ProtocolType.Tcp)
- {
- ReceiveTimeout = 5500, SendTimeout = 5500
- };
+ cancellationTokenSource?.Cancel();
- await Dispatcher.UIThread.InvokeAsync(() =>
+ if (rpc is { IsDisposed: false })
{
- Status = "Attempting to connect to server...";
- });
+ rpc.ClearPresence();
+ rpc.Dispose();
+ }
- var localEndPoint = new IPEndPoint(resolvedIpAddress!, 0xCAFE);
+ rpc = null;
- await client.ConnectAsync(localEndPoint, cancellationToken);
- await Dispatcher.UIThread.InvokeAsync(() => Status = "Connected to the server!");
- await UpdateConnectionStatusAsync(true);
+ if (client != null)
+ {
+ client.Close();
+ client.Dispose();
+ }
- await DataListenAsync(cancellationToken);
- }
- catch (ArgumentNullException)
- {
- await Task.Delay(1000, cancellationToken);
- resolvedIpAddress = IPAddress.Parse(Utils.GetIpByMac(ipAddress));
- }
- catch (SocketException)
- {
- client?.Close();
- if (rpc != null && !rpc.IsDisposed) rpc.ClearPresence();
- await Task.Delay(5000, cancellationToken);
+ client = null;
+
+ isConnected = false;
+ Status = "Disconnected";
+ lastProgramName = string.Empty;
+ time = null;
+ _ = UpdateConnectionStatusAsync(false);
}
catch (Exception ex)
{
- await Dispatcher.UIThread.InvokeAsync(() => Status = $"Connection error: {ex.Message}");
- await Task.Delay(5000, cancellationToken);
+ Status = $"Error during disconnect: {ex.Message}";
}
}
- }
- private async Task DataListenAsync(CancellationToken cancellationToken)
- {
- manualUpdate = true;
- while (!cancellationToken.IsCancellationRequested)
+ private async Task TryConnect(CancellationToken cancellationToken)
{
- try
+ rpc = new DiscordRpcClient(clientId);
+ rpc.Initialize();
+
+ while (!cancellationToken.IsCancellationRequested)
{
- var bytes = await PresenceCommon.Utils.ReceiveExactlyAsync(client!,
- cancellationToken: cancellationToken);
- await Dispatcher.UIThread.InvokeAsync(() => Status = "Connected to the server!");
+ try
+ {
+ client = new Socket(SocketType.Stream, ProtocolType.Tcp)
+ {
+ ReceiveTimeout = 5500, SendTimeout = 5500
+ };
+
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ Status = "Attempting to connect to server...";
+ });
+
+ var localEndPoint = new IPEndPoint(resolvedIpAddress!, 0xCAFE);
- var title = new Title(bytes);
- if (title.Magic == 0xffaadd23)
+ await client.ConnectAsync(localEndPoint, cancellationToken);
+ await Dispatcher.UIThread.InvokeAsync(() => Status = "Connected to the server!");
+ await UpdateConnectionStatusAsync(true);
+
+ await DataListenAsync(cancellationToken);
+ }
+ catch (ArgumentNullException)
{
- if (lastProgramName != title.Name)
+ await Task.Delay(1000, cancellationToken);
+ var newIp = NetworkUtils.GetIpByMac(ipAddress);
+ if (!string.IsNullOrEmpty(newIp))
{
- time = Timestamps.Now;
+ resolvedIpAddress = IPAddress.Parse(newIp);
}
+ }
+ catch (SocketException)
+ {
+ if (client != null)
+ {
+ client.Close();
+ client.Dispose();
+ client = null;
+ }
+
+ if (rpc is { IsDisposed: false }) rpc.ClearPresence();
+ await Task.Delay(5000, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ await Dispatcher.UIThread.InvokeAsync(() => Status = $"Connection error: {ex.Message}");
+ await Task.Delay(5000, cancellationToken);
+ }
+ }
+ }
- if (lastProgramName != title.Name || manualUpdate)
+ private async Task DataListenAsync(CancellationToken cancellationToken)
+ {
+ manualUpdate = true;
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ var bytes = await PresenceCommon.Utils.ReceiveExactlyAsync(client!,
+ cancellationToken: cancellationToken);
+ await Dispatcher.UIThread.InvokeAsync(() => Status = "Connected to the server!");
+
+ var title = new Title(bytes);
+ if (title.Magic == 0xffaadd23)
{
- await UpdatePropertiesFromTitle(title);
+ if (lastProgramName != title.Name)
+ {
+ time = Timestamps.Now;
+ }
- if (rpc != null)
+ if (lastProgramName != title.Name || manualUpdate)
{
- if (!DisplayHomeMenu && title.Name == "Home Menu")
- rpc.ClearPresence();
- else
+ await UpdatePropertiesFromTitle(title);
+
+ if (rpc is { IsDisposed: false })
{
- rpc.SetPresence(PresenceCommon.Utils.CreateDiscordPresence(title, time, BigImageKey,
- BigImageText, SmallImageKey, StateText, ShowTimeLapsed));
+ if (!DisplayHomeMenu && title.Name == "Home Menu")
+ rpc.ClearPresence();
+ else
+ {
+ rpc.SetPresence(PresenceCommon.Utils.CreateDiscordPresence(title, time, BigImageKey,
+ BigImageText, SmallImageKey, StateText, ShowTimeLapsed));
+ }
}
- }
- manualUpdate = false;
- lastProgramName = title.Name;
+ manualUpdate = false;
+ lastProgramName = title.Name;
+ }
+ }
+ else
+ {
+ if (rpc is { IsDisposed: false }) rpc.ClearPresence();
+ if (client == null) return;
+ client.Close();
+ client.Dispose();
+ return;
}
}
- else
+ catch (SocketException)
+ {
+ if (rpc is { IsDisposed: false }) rpc.ClearPresence();
+ if (client == null) return;
+ client.Close();
+ client.Dispose();
+ return;
+ }
+ catch (Exception ex)
{
- if (rpc != null && !rpc.IsDisposed) rpc.ClearPresence();
- client!.Close();
+ await Dispatcher.UIThread.InvokeAsync(() => Status = $"Error during data listen: {ex.Message}");
return;
}
}
- catch (SocketException)
+ }
+
+ private async Task UpdatePropertiesFromTitle(Title title)
+ {
+ await Dispatcher.UIThread.InvokeAsync(() =>
{
- if (rpc != null && !rpc.IsDisposed) rpc.ClearPresence();
- client!.Close();
- return;
+ BigImageKey = $"0{title.ProgramId:x}";
+ BigImageText = title.Name;
+ StateText = $"Playing {title.Name}";
+ });
+ }
+
+ private async Task IpToMacAsync()
+ {
+ if (resolvedIpAddress == null) return;
+
+ var macAddress = NetworkUtils.GetMacByIp(resolvedIpAddress.ToString());
+ if (!string.IsNullOrEmpty(macAddress))
+ {
+ ipAddress = macAddress;
+ }
+ else
+ {
+ await Dispatcher.UIThread.InvokeAsync(() => Status = "Can't convert to MAC Address! Sorry!");
}
}
- }
- private async Task UpdatePropertiesFromTitle(Title title)
- {
- await Dispatcher.UIThread.InvokeAsync(() =>
+ private void LoadConfig()
{
- BigImageKey = $"0{title.ProgramId:x}";
- BigImageText = title.Name;
- StateText = $"Playing {title.Name}";
- });
- }
+ try
+ {
+ if (!File.Exists(_configPath)) return;
+
+ var jsonString = File.ReadAllText(_configPath);
+ var cfg = JsonSerializer.Deserialize(jsonString, SourceGenerationContext.Default.Config);
+ if (cfg == null) return;
+
+ showTimeLapsed = cfg.DisplayTimer;
+ bigImageKey = cfg.BigKey;
+ bigImageText = cfg.BigText;
+ smallImageKey = cfg.SmallKey;
+ ipAddress = cfg.Ip;
+ stateText = cfg.State;
+ clientId = cfg.Client;
+ minimizeToTray = cfg.AllowTray;
+ displayHomeMenu = cfg.DisplayMainMenu;
+ hasSeenMacPrompt = cfg.SeenAutoMacPrompt;
+ autoConvertIpToMac = cfg.AutoToMac;
+ trayIconManager?.EnableTrayIcon(cfg.AllowTray);
+ }
+ catch (Exception ex)
+ {
+ Status = $"Error loading config: {ex.Message}";
+ minimizeToTray = false;
+ }
+ }
- private async Task IpToMacAsync()
- {
- var macAddress = Utils.GetMacByIp(resolvedIpAddress!.ToString());
- if (macAddress != string.Empty)
- ipAddress = macAddress;
- else
+
+ private void SaveConfig()
{
- await Dispatcher.UIThread.InvokeAsync(() => Status = "Can't convert to MAC Address! Sorry!");
+ try
+ {
+ PlatformHelper.EnsureConfigDirectory();
+
+ var cfg = new Config
+ {
+ Ip = ipAddress,
+ Client = clientId,
+ BigKey = bigImageKey,
+ SmallKey = smallImageKey,
+ State = stateText,
+ BigText = bigImageText,
+ DisplayTimer = showTimeLapsed,
+ AllowTray = minimizeToTray,
+ DisplayMainMenu = displayHomeMenu,
+ SeenAutoMacPrompt = hasSeenMacPrompt,
+ AutoToMac = autoConvertIpToMac
+ };
+
+ var jsonString = JsonSerializer.Serialize(cfg, SourceGenerationContext.Default.Config);
+ File.WriteAllText(_configPath, jsonString);
+ trayIconManager?.EnableTrayIcon(minimizeToTray);
+ }
+ catch (Exception ex)
+ {
+ Status = $"Error saving config: {ex.Message}";
+ Console.WriteLine("Error saving config: " + ex.Message);
+ }
}
}
- private void LoadConfig()
- {
- if (!File.Exists("Config.json")) return;
- var cfg = JsonConvert.DeserializeObject(File.ReadAllText("Config.json"));
- if (cfg == null) return;
- showTimeLapsed = cfg.DisplayTimer;
- bigImageKey = cfg.BigKey;
- bigImageText = cfg.BigText;
- smallImageKey = cfg.SmallKey;
- ipAddress = cfg.Ip;
- stateText = cfg.State;
- clientId = cfg.Client;
- minimizeToTray = cfg.AllowTray;
- displayHomeMenu = cfg.DisplayMainMenu;
- hasSeenMacPrompt = cfg.SeenAutoMacPrompt;
- autoConvertIpToMac = cfg.AutoToMac;
- }
+ [JsonSourceGenerationOptions(WriteIndented = true)]
+ [JsonSerializable(typeof(Config))]
+ internal partial class SourceGenerationContext : JsonSerializerContext { }
- private void SaveConfig()
+ public class Config
{
- var cfg = new Config
- {
- Ip = ipAddress,
- Client = clientId,
- BigKey = bigImageKey,
- SmallKey = smallImageKey,
- State = stateText,
- BigText = bigImageText,
- DisplayTimer = showTimeLapsed,
- AllowTray = minimizeToTray,
- DisplayMainMenu = displayHomeMenu,
- SeenAutoMacPrompt = hasSeenMacPrompt,
- AutoToMac = autoConvertIpToMac
- };
- File.WriteAllText("Config.json", JsonConvert.SerializeObject(cfg, Formatting.Indented));
+ public string Ip { get; set; } = "";
+ public string Client { get; set; } = "";
+ public string BigKey { get; set; } = "";
+ public string SmallKey { get; set; } = "";
+ public string State { get; set; } = "";
+ public string BigText { get; set; } = "";
+ public bool DisplayTimer { get; set; }
+ public bool AllowTray { get; set; }
+ public bool DisplayMainMenu { get; set; }
+ public bool SeenAutoMacPrompt { get; set; }
+ public bool AutoToMac { get; set; }
+
+ public Config()
+ {
+ DisplayMainMenu = true;
+ DisplayTimer = true;
+ }
}
-}
-
-public class Config
-{
- public string Ip { get; set; } = "";
- public string Client { get; set; } = "";
- public string BigKey { get; set; } = "";
- public string SmallKey { get; set; } = "";
- public string State { get; set; } = "";
- public string BigText { get; set; } = "";
- public bool DisplayTimer { get; set; }
- public bool AllowTray { get; set; }
- public bool DisplayMainMenu { get; set; }
- public bool SeenAutoMacPrompt { get; set; }
- public bool AutoToMac { get; set; }
}
\ No newline at end of file
diff --git a/PresenceClient/PresenceClient-GUI/Views/MainWindow.axaml.cs b/PresenceClient/PresenceClient-GUI/Views/MainWindow.axaml.cs
index 7dc2b99..33af3c0 100644
--- a/PresenceClient/PresenceClient-GUI/Views/MainWindow.axaml.cs
+++ b/PresenceClient/PresenceClient-GUI/Views/MainWindow.axaml.cs
@@ -3,10 +3,10 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using PresenceClient.ViewModels;
+using PresenceClient.Platform;
namespace PresenceClient.Views;
-
-public partial class MainWindow : Window, IDisposable
+public partial class MainWindow : Window
{
private MainWindowViewModel? viewModel;
@@ -16,19 +16,43 @@ public MainWindow()
#if DEBUG
this.AttachDevTools();
#endif
+
+ if (PlatformHelper.IsMacOS)
+ {
+ this.ExtendClientAreaToDecorationsHint = true;
+ this.ExtendClientAreaChromeHints = Avalonia.Platform.ExtendClientAreaChromeHints.OSXThickTitleBar;
+ }
+
viewModel = new MainWindowViewModel();
DataContext = viewModel;
viewModel.ShowMainWindowRequested += (sender, args) => ShowMainWindow();
+
+ SetupPlatformSpecifics();
}
- private void InitializeComponent()
+ private void SetupPlatformSpecifics()
{
- AvaloniaXamlLoader.Load(this);
+ if (PlatformHelper.IsMacOS)
+ {
+ var mainMenu = new NativeMenu();
+ var appMenu = new NativeMenu();
+ var appMenuItem = new NativeMenuItem("PresenceClient");
+ appMenuItem.Menu = appMenu;
+
+ appMenu.Add(new NativeMenuItemSeparator());
+
+ var quitItem = new NativeMenuItem("Quit PresenceClient");
+ quitItem.Click += (sender, e) => viewModel?.ExitApplication();
+ appMenu.Add(quitItem);
+
+ mainMenu.Add(appMenuItem);
+ NativeMenu.SetMenu(this, mainMenu);
+ }
}
protected override void OnClosing(WindowClosingEventArgs e)
{
- if (viewModel != null && viewModel.MinimizeToTray)
+ if (viewModel is { MinimizeToTray: true } && PlatformHelper.CanUseTrayIcon())
{
e.Cancel = true;
this.Hide();
@@ -36,7 +60,7 @@ protected override void OnClosing(WindowClosingEventArgs e)
else
{
base.OnClosing(e);
- Dispose();
+ viewModel?.ExitApplication(); // This will properly clean up and exit
}
}
@@ -49,17 +73,13 @@ public void ShowMainWindow()
this.Show();
this.Activate();
- this.Topmost = true;
- this.Topmost = false;
- this.Focus();
- }
- public void Dispose()
- {
- if (viewModel != null)
+ if (!PlatformHelper.IsMacOS) // macOS handles window focusing differently
{
- viewModel.Dispose();
- viewModel = null;
+ this.Topmost = true;
+ this.Topmost = false;
}
+
+ this.Focus();
}
}
\ No newline at end of file