using System; using System.Diagnostics; using System.Runtime.InteropServices; using MelonLoader; namespace ADOFAIRenderer.Util { public static class OSNotification { /// /// Sends a native desktop notification on Windows, macOS, or Linux. /// If a folder path is provided, it attempts to add an action button to open the folder on Windows and Linux. /// public static void Show(string title, string message, string? folderPath = null) { try { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { ShowWindowsNotification(title, message, folderPath); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { ShowMacNotification(title, message); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { ShowLinuxNotification(title, message, folderPath); } } catch (Exception ex) { MelonLogger.Warning($"[OSNotification] 알림을 보내지 못했습니다: {ex.Message}"); } } private static void ShowWindowsNotification(string title, string message, string? folderPath) { // XML-escape the title and message to prevent XML syntax errors (e.g. from '&', '<', '>', '"', '\'') string xmlTitle = System.Security.SecurityElement.Escape(title)!; string xmlMessage = System.Security.SecurityElement.Escape(message)!; string toastXml; string psScript; if (!string.IsNullOrEmpty(folderPath)) { var nonNullFolder = folderPath; var escapedFolder = nonNullFolder.Replace("'", "''"); // UWP Toast XML using protocol activation. // Clicking the body (launch) or the button (arguments) will invoke the custom 'adofai-renderer://' protocol. toastXml = $@" {xmlTitle} {xmlMessage} ".Replace("\r\n", "").Replace("\n", "").Trim(); // Build a PowerShell script that registers a custom protocol 'adofai-renderer' in HKCU (no admin rights needed), // mapping it to invoke PowerShell and open the folder, then sends the notification and exits immediately. psScript = $"$protocolPath = 'HKCU:\\Software\\Classes\\adofai-renderer';\n" + $"if (-not (Test-Path $protocolPath)) {{\n" + $" $null = New-Item -Path $protocolPath -Force;\n" + $" $null = New-ItemProperty -Path $protocolPath -Name 'URL Protocol' -Value '' -PropertyType String -Force;\n" + $"}}\n" + $"Set-Item -Path $protocolPath -Value 'URL:adofai-renderer Protocol' | Out-Null;\n" + $"$cmdPath = \"$protocolPath\\shell\\open\\command\";\n" + $"if (-not (Test-Path $cmdPath)) {{ $null = New-Item -Path $cmdPath -Force; }}\n" + $"$command = 'powershell.exe -NoProfile -NonInteractive -WindowStyle Hidden -Command \"Invoke-Item ''{escapedFolder}''\"';\n" + $"Set-Item -Path $cmdPath -Value $command | Out-Null;\n" + $"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null;\n" + $"$xml = [Windows.Data.Xml.Dom.XmlDocument]::new();\n" + $"$xml.LoadXml('{toastXml}');\n" + $"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('ADOFAI Renderer').Show([Windows.UI.Notifications.ToastNotification]::new($xml))"; } else { // Simple UWP Toast XML without actions. toastXml = $@" {xmlTitle} {xmlMessage} ".Replace("\r\n", "").Replace("\n", "").Trim(); psScript = $"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null;\n" + $"$xml = [Windows.Data.Xml.Dom.XmlDocument]::new();\n" + $"$xml.LoadXml('{toastXml}');\n" + $"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('ADOFAI Renderer').Show([Windows.UI.Notifications.ToastNotification]::new($xml))"; } // Encode the script to UTF-16LE Base64 and run it via PowerShell -EncodedCommand. // This guarantees that any special characters (double quotes, backslashes, ampersands, etc.) in the title/message // do not break the command-line parsing or cause syntax errors. byte[] bytes = System.Text.Encoding.Unicode.GetBytes(psScript); string base64 = Convert.ToBase64String(bytes); RunProcess("powershell.exe", $"-NoProfile -NonInteractive -EncodedCommand {base64}"); } private static void ShowMacNotification(string title, string message) { string escapedTitle = title.Replace("\"", "\\\""); string escapedMessage = message.Replace("\"", "\\\""); string args = $"-e \"display notification \\\"{escapedMessage}\\\" with title \\\"{escapedTitle}\\\"\""; RunProcess("osascript", args); } private static void ShowLinuxNotification(string title, string message, string? folderPath) { var escapedTitle = title.Replace("\"", "\\\""); var escapedMessage = message.Replace("\"", "\\\""); if (!string.IsNullOrEmpty(folderPath)) { var nonNullFolder = folderPath; // Linux notify-send supports action buttons since v1.2. // It prints the action name to standard output when clicked. var args = $"-a \"ADOFAI Renderer\" --action=\"open_folder=렌더 폴더 열기\" \"{escapedTitle}\" \"{escapedMessage}\""; System.Threading.Tasks.Task.Run(() => { try { var startInfo = new ProcessStartInfo { FileName = "notify-send", Arguments = args, CreateNoWindow = true, UseShellExecute = false, RedirectStandardOutput = true, WindowStyle = ProcessWindowStyle.Hidden }; using var process = new Process(); process.StartInfo = startInfo; process.Start(); string output = process.StandardOutput.ReadToEnd().Trim(); // If user clicked the button, open the folder using default file manager if (output == "open_folder") { RunProcess("xdg-open", $"\"{nonNullFolder.Replace("\"", "\\\"")}\""); } } catch (Exception ex) { MelonLogger.Warning($"[OSNotification] 리눅스 알림 처리 실패: {ex.Message}"); } }); } else { var args = $"-a \"ADOFAI Renderer\" \"{escapedTitle}\" \"{escapedMessage}\""; RunProcess("notify-send", args); } } private static void RunProcess(string fileName, string arguments) { var startInfo = new ProcessStartInfo { FileName = fileName, Arguments = arguments, CreateNoWindow = true, // CREATE_NO_WINDOW flag UseShellExecute = false, // Direct execution WindowStyle = ProcessWindowStyle.Hidden }; using var process = new Process(); process.StartInfo = startInfo; process.Start(); } } }