Files
ADOFAIRenderer/Util/OSNotification.cs
T

190 lines
8.5 KiB
C#

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using MelonLoader;
namespace ADOFAIRenderer.Util
{
public static class OSNotification
{
/// <summary>
/// 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.
/// </summary>
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 = $@"
<toast launch=""adofai-renderer://"" activationType=""protocol"">
<visual>
<binding template=""ToastGeneric"">
<text>{xmlTitle}</text>
<text>{xmlMessage}</text>
</binding>
</visual>
<actions>
<action content=""렌더 폴더 열기"" arguments=""adofai-renderer://"" activationType=""protocol""/>
</actions>
</toast>
".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 = $@"
<toast>
<visual>
<binding template=""ToastGeneric"">
<text>{xmlTitle}</text>
<text>{xmlMessage}</text>
</binding>
</visual>
</toast>
".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();
}
}
}