Files
MaaAssistantArknights/src/MaaWpfGui/Helper/ToastNotification.cs
status102 f4b71bd0be refactor: 新Config结构修改
部分迁移自 #12315
2025-07-11 09:09:51 +08:00

565 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// <copyright file="ToastNotification.cs" company="MaaAssistantArknights">
// Part of the MaaWpfGui project, maintained by the MaaAssistantArknights team (Maa Team)
// Copyright (C) 2021-2025 MaaAssistantArknights Contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License v3.0 only as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY
// </copyright>
#pragma warning disable SA1307
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Media;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Interop;
using HandyControl.Controls;
using MaaWpfGui.Configuration.Factory;
using MaaWpfGui.Helper.Notification;
using MaaWpfGui.WineCompat;
using Microsoft.Win32;
using Notification.Wpf.Constants;
using Serilog;
using Stylet;
namespace MaaWpfGui.Helper
{
/// <summary>
/// The toast notification.
/// </summary>
public class ToastNotification : IDisposable
{
private unsafe struct OSVERSIONINFOEXW
{
public int dwOSVersionInfoSize;
public int dwMajorVersion;
public int dwMinorVersion;
public int dwBuildNumber;
public int dwPlatformId;
private fixed char _szCSDVersion[128];
public short wServicePackMajor;
public short wServicePackMinor;
public short wSuiteMask;
public byte wProductType;
public byte wReserved;
public Span<char> szCSDVersion => MemoryMarshal.CreateSpan(ref _szCSDVersion[0], 128);
}
[DllImport("ntdll.dll", ExactSpelling = true)]
private static extern int RtlGetVersion(ref OSVERSIONINFOEXW versionInfo);
private static INotificationPoster _notificationPoster;
private static readonly string _openUrlPrefix;
static ToastNotification()
{
_notificationPoster = GetNotificationPoster();
_notificationPoster.ActionActivated += OnActionActivated;
_openUrlPrefix = $"OpenUrl:{_notificationPoster.GetHashCode()}:";
}
private static readonly ILogger _logger = Log.ForContext<ToastNotification>();
private static unsafe bool IsWindows10OrGreater()
{
var osVersionInfo = default(OSVERSIONINFOEXW);
osVersionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEXW);
RtlGetVersion(ref osVersionInfo);
var version = new Version(osVersionInfo.dwMajorVersion, osVersionInfo.dwMinorVersion, osVersionInfo.dwBuildNumber);
return version > new Version(10, 0, 10240);
}
/// <summary>
/// Checks toast system.
/// </summary>
/// <returns>The toast system is initialized.</returns>
private static INotificationPoster GetNotificationPoster()
{
if (WineRuntimeInformation.IsRunningUnderWine)
{
if (NotificationImplLibNotify.IsSupported)
{
return new NotificationImplLibNotify();
}
}
else if (IsWindows10OrGreater())
{
return new NotificationImplWinRT();
}
return new NotificationImplWpf();
}
/// <summary>
/// 按钮激活后的事件,参数为按钮标签
/// </summary>
public static event EventHandler<string> ActionActivated;
/// <summary>
/// 通知标题
/// </summary>
private string _notificationTitle = string.Empty;
/// <summary>
/// 通知文本内容
/// </summary>
private StringBuilder _contentCollection = new StringBuilder();
/// <summary>
/// Initializes a new instance of the <see cref="ToastNotification"/> class.
/// </summary>
/// <param name="title">通知标题</param>
/// <remarks>
/// <para>Toast通知基于 <see cref="NotificationConstants"/> 实现,方便管理通知样式。</para>
/// <para>建议使用 <see langword="using"/> 调用,如果不使用 <see langword="using"/> 调用,建议手动调用 <see cref="Dispose()"/> 方法释放。</para>
/// <para>在线程中必须使用 <c>Dispatcher.Invoke</c> 相关方法调用。</para>
/// </remarks>
public ToastNotification(string title = null)
{
// 初始化通知标题
_notificationTitle = title ?? _notificationTitle;
}
private static void OnActionActivated(object sender, string tag)
{
string decodedTag = WebUtility.UrlDecode(tag);
if (decodedTag.StartsWith(_openUrlPrefix))
{
var url = decodedTag[_openUrlPrefix.Length..];
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
else
{
ActionActivated?.Invoke(sender, decodedTag);
}
}
/// <summary>
/// 添加一行文本内容
/// </summary>
/// <param name="text">文本内容</param>
/// <returns>返回本类,可继续调用其它方法</returns>
public ToastNotification AppendContentText(string text = null)
{
_contentCollection.AppendLine(text ?? string.Empty);
return this;
}
#region
#region
/// <summary>
/// 通知提示音列表枚举,
/// </summary>
public enum NotificationSounds
{
/// <summary>
/// 默认响声。
/// </summary>
[Description("默认响声")]
Beep,
/// <summary>
/// 感叹号。
/// </summary>
[Description("感叹号")]
Exclamation,
/// <summary>
/// 星号。
/// </summary>
[Description("星号")]
Asterisk,
/// <summary>
/// 问题。
/// </summary>
[Description("问题")]
Question,
/// <summary>
/// 关键性停止。
/// </summary>
[Description("关键性停止")]
Hand,
/// <summary>
/// 通知 (Windows 10 及以上特有,低版本系统直接用也可以)。
/// </summary>
[Description("通知 (Windows 10 及以上特有,低版本系统直接用也可以)")]
Notification,
/// <summary>
/// 无声。
/// </summary>
[Description("无声")]
None,
}
#endregion
/// <summary>
/// 播放通知提示音
/// 如果要播放音频文件,参考微软文档 <see cref="SoundPlayer"/> 类
/// </summary>
/// <param name="sounds">提示音类型</param>
protected void PlayNotificationSound(NotificationSounds sounds = NotificationSounds.None)
{
try
{
switch (sounds)
{
case NotificationSounds.Exclamation: SystemSounds.Exclamation.Play(); break;
case NotificationSounds.Asterisk: SystemSounds.Asterisk.Play(); break;
case NotificationSounds.Question: SystemSounds.Question.Play(); break;
case NotificationSounds.Beep: SystemSounds.Beep.Play(); break;
case NotificationSounds.Hand: SystemSounds.Hand.Play(); break;
case NotificationSounds.Notification:
using (var key = Registry.CurrentUser.OpenSubKey(@"AppEvents\Schemes\Apps\.Default\Notification.Default\.Current"))
{
if (key != null)
{
// 获取 (Default) 项
var o = key.GetValue(null);
if (o != null)
{
var theSound = new SoundPlayer((string)o);
theSound.Play();
}
}
else
{
// 如果不支持就播放其它提示音
PlayNotificationSound(NotificationSounds.Asterisk);
}
}
break;
case NotificationSounds.None:
default: break;
}
}
catch (Exception)
{
// Ignore
}
}
#endregion
#region
#region
private List<NotificationAction> _actions = new List<NotificationAction>();
#endregion
/// <summary>
/// 给通知添加一个按钮
/// </summary>
/// <param name="label">按钮标题</param>
/// <param name="tag">事件标签</param>
/// <returns>返回类本身,可继续调用其它方法</returns>
/// <remarks>按钮按下时,触发 <see cref="ActionActivated"/> 事件</remarks>
public ToastNotification AddButton(string label, string tag)
{
_actions.Add(new NotificationAction(label, tag));
return this;
}
public static string GetActionTagForOpenWeb(string url)
{
if (url.StartsWith("http://") || url.StartsWith("https://"))
{
return _openUrlPrefix + url;
}
throw new ArgumentException("URL must start with http:// or https://");
}
#endregion
#region
#region
/// <summary>
/// 显示通知
/// </summary>
/// <param name="lifeTime">通知显示时间 (s)</param>
/// <param name="row">内容显示行数,如果内容太多建议使用 <see cref="ShowMore(double, uint, NotificationSounds, NotificationContent)"/></param>
/// <param name="sound">播放提示音</param>
/// <param name="hints">通知额外元数据,可能影响通知展示方式</param>
///
public void Show(double lifeTime = 10d, uint row = 1,
NotificationSounds sound = NotificationSounds.Notification, params NotificationHint[] hints)
{
Execute.OnUIThread(() =>
{
// TODO: 整理过时代码
if (!ConfigFactory.Root.GUI.UseNotify)
{
Growl.Info(_notificationTitle + _contentCollection);
return;
}
var content = new NotificationContent { Summary = _notificationTitle, Body = _contentCollection.ToString(), };
foreach (var action in _actions)
{
content.Actions.Add(action);
}
content.Hints.Add(NotificationHint.RowCount((int)row));
// 调整显示时间,如果存在按钮的情况下显示时间将强制设为最大时间
lifeTime = lifeTime < 3d ? 3d : lifeTime;
var timeSpan = _actions.Count != 0
? TimeSpan.FromSeconds(lifeTime)
: TimeSpan.MaxValue;
content.Hints.Add(NotificationHint.ExpirationTime(timeSpan));
foreach (var hint in hints)
{
content.Hints.Add(hint);
}
// 显示通知
_notificationPoster.ShowNotification(content);
// 播放通知提示音
PlayNotificationSound(sound);
// 任务栏闪烁
FlashWindowEx();
});
}
/// <summary>
/// 显示内容更多的通知
/// </summary>
/// <param name="lifeTime">通知显示时间 (s)</param>
/// <param name="row">内容显示行数,只用于预览一部分通知,多出内容会放在附加按钮的窗口中</param>
/// <param name="sound">播放提示音,不设置就没有声音</param>
public void ShowMore(double lifeTime = 12d, uint row = 2,
NotificationSounds sound = NotificationSounds.None, params NotificationHint[] hints)
{
var morehints = new NotificationHint[hints.Length + 1];
hints.CopyTo(morehints, 0);
morehints[hints.Length] = NotificationHint.Expandable;
Show(lifeTime: lifeTime,
row: row,
sound: sound,
hints: morehints);
}
/// <summary>
/// 显示公招特殊标签通知
/// </summary>
/// <param name="row">内容显示行数,比如第 2 行用来放星星</param>
public void ShowRecruit(uint row = 1)
{
Show(row: row,
sound: NotificationSounds.Notification,
hints: NotificationHint.RecruitHighRarity);
}
/// <summary>
/// 显示公招小车标签通知
/// </summary>
/// <param name="row">内容显示行数,比如第 2 行用来放星星</param>
public void ShowRecruitRobot(uint row = 1)
{
Show(row: row,
sound: NotificationSounds.Notification,
hints: NotificationHint.RecruitRobot);
}
/// <summary>
/// 显示更新版本的通知
/// </summary>
/// <param name="row">内容行数</param>
public void ShowUpdateVersion(uint row = 3)
{
ShowMore(row: row,
sound: NotificationSounds.Notification,
hints: NotificationHint.NewVersion);
}
/// <summary>
/// 直接显示通知内容
/// </summary>
/// <param name="message">显示内容</param>
public static void ShowDirect(string message)
{
using var toast = new ToastNotification(message);
toast.Show();
}
#endregion
#endregion
#region
/// <summary>
/// 闪烁信息
/// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "NotAccessedField.Local")]
private struct FLASHWINFO
{
/// <summary>
/// 结构大小
/// </summary>
public uint cbSize;
/// <summary>
/// 要闪烁或停止的窗口句柄
/// </summary>
public IntPtr hwnd;
/// <summary>
/// 闪烁的类型
/// </summary>
public uint dwFlags;
/// <summary>
/// 闪烁窗口的次数
/// </summary>
public uint uCount;
/// <summary>
/// 窗口闪烁的频率(毫秒)
/// </summary>
public uint dwTimeout;
}
#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter
/// <summary>
/// 闪烁类型。
/// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "IdentifierTypo")]
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public enum FlashType : uint
{
/// <summary>
/// 停止闪烁。
/// </summary>
[Description("停止闪烁")]
FLASHW_STOP = 0,
/// <summary>
/// 只闪烁标题。
/// </summary>
[Description("只闪烁标题")]
FALSHW_CAPTION = 1,
/// <summary>
/// 只闪烁任务栏。
/// </summary>
[Description("只闪烁任务栏")]
FLASHW_TRAY = 2,
/// <summary>
/// 标题和任务栏同时闪烁。
/// </summary>
[Description("标题和任务栏同时闪烁")]
FLASHW_ALL = 3,
/// <summary>
/// 与 <see cref="FLASHW_TRAY"/> 配合使用。
/// </summary>
FLASHW_PARAM1 = 4,
/// <summary>
/// 与 <see cref="FLASHW_TRAY"/> 配合使用。
/// </summary>
FLASHW_PARAM2 = 12,
/// <summary>
/// 闪烁直到达到次数或收到停止。
/// </summary>
[Description("闪烁直到达到次数或收到停止")]
FLASHW_TIMER = FLASHW_TRAY | FLASHW_PARAM1,
/// <summary>
/// 未激活时闪烁直到窗口被激活或收到停止。
/// </summary>
[Description("未激活时闪烁直到窗口被激活或收到停止")]
FLASHW_TIMERNOFG = FLASHW_TRAY | FLASHW_PARAM2,
}
[DllImport("user32.dll")]
private static extern bool FlashWindowEx(ref FLASHWINFO pwfi);
/// <summary>
/// 闪烁窗口任务栏
/// </summary>
/// <param name="hWnd">窗口句柄</param>
/// <param name="type">闪烁类型</param>
/// <param name="count">闪烁次数</param>
/// <returns>是否成功</returns>
public static bool FlashWindowEx(IntPtr hWnd = default, FlashType type = FlashType.FLASHW_TIMERNOFG, uint count = 5)
{
try
{
var fInfo = default(FLASHWINFO);
fInfo.cbSize = Convert.ToUInt32(Marshal.SizeOf(fInfo));
fInfo.hwnd = hWnd != default ? hWnd : new WindowInteropHelper(System.Windows.Application.Current.MainWindow!).Handle;
fInfo.dwFlags = (uint)type;
fInfo.uCount = count;
fInfo.dwTimeout = 0;
return FlashWindowEx(ref fInfo);
}
catch
{
_logger.Warning("FlashWindowEx error");
return false;
}
}
#endregion
/// <summary>
/// 通知使用完后释放已使用的数据
/// </summary>
public void Dispose()
{
_contentCollection.Clear();
}
public static void Cleanup()
{
try
{
(_notificationPoster as IDisposable)?.Dispose();
}
catch (Exception e)
{
Log.ForContext<ToastNotification>().Error(e, "Cleanup error");
}
}
}
}