diff --git a/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs b/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs
index 11d04b1..022b5a6 100644
--- a/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs
+++ b/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs
@@ -45,4 +45,5 @@ public static class BuiltInComponentIds
public const string DesktopRemovableStorage = "DesktopRemovableStorage";
public const string DesktopZhiJiaoHub = "DesktopZhiJiaoHub";
public const string DesktopFileManager = "DesktopFileManager";
+ public const string DesktopNotificationBox = "DesktopNotificationBox";
}
diff --git a/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs b/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs
index 5234499..7a784b4 100644
--- a/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs
+++ b/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs
@@ -410,6 +410,16 @@ public sealed class ComponentRegistry
MinHeightCells: 4,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true,
+ ResizeMode: DesktopComponentResizeMode.Free),
+ new DesktopComponentDefinition(
+ BuiltInComponentIds.DesktopNotificationBox,
+ "消息盒子",
+ "Inbox",
+ "Info",
+ MinWidthCells: 2,
+ MinHeightCells: 2,
+ AllowStatusBarPlacement: false,
+ AllowDesktopPlacement: true,
ResizeMode: DesktopComponentResizeMode.Free)
};
diff --git a/LanMountainDesktop/LanMountainDesktop.csproj b/LanMountainDesktop/LanMountainDesktop.csproj
index 58fd65e..67a3347 100644
--- a/LanMountainDesktop/LanMountainDesktop.csproj
+++ b/LanMountainDesktop/LanMountainDesktop.csproj
@@ -76,6 +76,7 @@
+
diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs
index f01fbd4..e3e0490 100644
--- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs
+++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs
@@ -200,6 +200,35 @@ public sealed class AppSettingsSnapshot
#endregion
+ #region Notification Box Settings (消息盒子全局设置)
+
+ ///
+ /// 启用消息盒子功能(Windows通知监听)
+ ///
+ public bool NotificationBoxEnabled { get; set; } = true;
+
+ ///
+ /// 隐私模式:开启后只显示"您有新的通知",不显示具体内容
+ ///
+ public bool NotificationBoxPrivacyMode { get; set; } = false;
+
+ ///
+ /// 被屏蔽的应用列表(不接收这些应用的通知)
+ ///
+ public List NotificationBoxBlockedApps { get; set; } = [];
+
+ ///
+ /// 历史记录保留天数
+ ///
+ public int NotificationBoxHistoryRetentionDays { get; set; } = 7;
+
+ ///
+ /// 最大存储通知数量(防止内存无限增长)
+ ///
+ public int NotificationBoxMaxStoredCount { get; set; } = 500;
+
+ #endregion
+
public AppSettingsSnapshot Clone()
{
var clone = (AppSettingsSnapshot)MemberwiseClone();
@@ -213,6 +242,9 @@ public sealed class AppSettingsSnapshot
clone.DisabledPluginIds = DisabledPluginIds is { Count: > 0 }
? new List(DisabledPluginIds)
: [];
+ clone.NotificationBoxBlockedApps = NotificationBoxBlockedApps is { Count: > 0 }
+ ? new List(NotificationBoxBlockedApps)
+ : [];
return clone;
}
diff --git a/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs b/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs
index 38461e8..0a2fd97 100644
--- a/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs
+++ b/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs
@@ -84,6 +84,45 @@ public sealed class ComponentSettingsSnapshot
public int ZhiJiaoHubCurrentImageIndex { get; set; } = 0;
+ #region Notification Box Component Settings (消息盒子组件设置)
+
+ ///
+ /// 组件内最大显示通知数量
+ ///
+ public int NotificationBoxMaxDisplayCount { get; set; } = 50;
+
+ ///
+ /// 排序方式:TimeDesc(时间倒序), TimeAsc(时间正序), AppGroup(按应用分组)
+ ///
+ public string NotificationBoxSortOrder { get; set; } = "TimeDesc";
+
+ ///
+ /// 是否显示应用图标
+ ///
+ public bool NotificationBoxShowAppIcon { get; set; } = true;
+
+ ///
+ /// 是否显示时间戳
+ ///
+ public bool NotificationBoxShowTimestamp { get; set; } = true;
+
+ ///
+ /// 时间格式:Relative(相对时间,如"5分钟前"), Absolute(绝对时间)
+ ///
+ public string NotificationBoxTimeFormat { get; set; } = "Relative";
+
+ ///
+ /// 是否按应用分组显示
+ ///
+ public bool NotificationBoxGroupByApp { get; set; } = false;
+
+ ///
+ /// 是否显示清除按钮
+ ///
+ public bool NotificationBoxShowClearButton { get; set; } = true;
+
+ #endregion
+
public ComponentSettingsSnapshot Clone()
{
var clone = (ComponentSettingsSnapshot)MemberwiseClone();
diff --git a/LanMountainDesktop/Models/NotificationItem.cs b/LanMountainDesktop/Models/NotificationItem.cs
new file mode 100644
index 0000000..119b067
--- /dev/null
+++ b/LanMountainDesktop/Models/NotificationItem.cs
@@ -0,0 +1,54 @@
+using System;
+
+namespace LanMountainDesktop.Models;
+
+///
+/// 通知项数据模型
+///
+public sealed class NotificationItem
+{
+ ///
+ /// 唯一标识
+ ///
+ public string Id { get; set; } = Guid.NewGuid().ToString();
+
+ ///
+ /// 应用ID(如 WeChat, Outlook 等)
+ ///
+ public string AppId { get; set; } = string.Empty;
+
+ ///
+ /// 应用名称
+ ///
+ public string AppName { get; set; } = string.Empty;
+
+ ///
+ /// 应用图标路径或Base64
+ ///
+ public string? AppIconPath { get; set; }
+
+ ///
+ /// 通知标题
+ ///
+ public string Title { get; set; } = string.Empty;
+
+ ///
+ /// 通知内容
+ ///
+ public string Content { get; set; } = string.Empty;
+
+ ///
+ /// 接收时间
+ ///
+ public DateTime ReceivedTime { get; set; } = DateTime.Now;
+
+ ///
+ /// 是否已读
+ ///
+ public bool IsRead { get; set; } = false;
+
+ ///
+ /// 原始通知的额外数据(用于点击跳转)
+ ///
+ public string? LaunchArgs { get; set; }
+}
diff --git a/LanMountainDesktop/Services/DesktopComponentEditorRegistryFactory.cs b/LanMountainDesktop/Services/DesktopComponentEditorRegistryFactory.cs
index ca72d46..6bd01be 100644
--- a/LanMountainDesktop/Services/DesktopComponentEditorRegistryFactory.cs
+++ b/LanMountainDesktop/Services/DesktopComponentEditorRegistryFactory.cs
@@ -267,6 +267,11 @@ public static class DesktopComponentEditorRegistryFactory
BuiltInComponentIds.DesktopZhiJiaoHub,
context => new ZhiJiaoHubComponentEditor(context),
preferredWidth: 480d,
+ preferredHeight: 520d),
+ [BuiltInComponentIds.DesktopNotificationBox] = new(
+ BuiltInComponentIds.DesktopNotificationBox,
+ context => new NotificationBoxComponentEditor(context),
+ preferredWidth: 480d,
preferredHeight: 520d)
};
diff --git a/LanMountainDesktop/Services/LinuxNotificationListener.cs b/LanMountainDesktop/Services/LinuxNotificationListener.cs
new file mode 100644
index 0000000..4c298b0
--- /dev/null
+++ b/LanMountainDesktop/Services/LinuxNotificationListener.cs
@@ -0,0 +1,216 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Runtime.Versioning;
+using System.Threading;
+using System.Threading.Tasks;
+using LanMountainDesktop.Models;
+
+namespace LanMountainDesktop.Services;
+
+///
+/// Linux平台通知监听器 - 通过DBus监听org.freedesktop.Notifications
+///
+[SupportedOSPlatform("linux")]
+internal sealed class LinuxNotificationListener : IDisposable
+{
+ private readonly NotificationListenerService _parent;
+ private CancellationTokenSource? _cts;
+ private bool _isRunning;
+
+ public LinuxNotificationListener(NotificationListenerService parent)
+ {
+ _parent = parent;
+ }
+
+ ///
+ /// 初始化并启动DBus监听
+ ///
+ public async Task InitializeAsync()
+ {
+ try
+ {
+ // 检查DBus环境变量
+ var dbusSessionBus = Environment.GetEnvironmentVariable("DBUS_SESSION_BUS_ADDRESS");
+ if (string.IsNullOrEmpty(dbusSessionBus))
+ {
+ Console.WriteLine("[NotificationBox] DBus Session Bus 环境变量未设置");
+ return false;
+ }
+
+ // 检查通知守护进程是否运行
+ // 通过检查常见进程名
+ var hasNotificationDaemon = await CheckNotificationDaemonAsync();
+ if (!hasNotificationDaemon)
+ {
+ Console.WriteLine("[NotificationBox] 未检测到通知守护进程,消息盒子功能可能不可用");
+ // 仍然返回true,因为守护进程可能在之后启动
+ }
+
+ _cts = new CancellationTokenSource();
+ _ = StartListeningAsync(_cts.Token);
+
+ Console.WriteLine("[NotificationBox] Linux通知监听已启动");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[NotificationBox] Linux通知监听初始化失败: {ex.Message}");
+ return false;
+ }
+ }
+
+ private async Task CheckNotificationDaemonAsync()
+ {
+ try
+ {
+ // 检查常见通知守护进程
+ var processNames = new[] { "gnome-shell", "kded5", "dunst", "mako", "swaync" };
+ foreach (var name in processNames)
+ {
+ var psi = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = "pgrep",
+ Arguments = $"-x {name}",
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var process = System.Diagnostics.Process.Start(psi);
+ if (process != null)
+ {
+ await process.WaitForExitAsync();
+ if (process.ExitCode == 0)
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private async Task StartListeningAsync(CancellationToken ct)
+ {
+ _isRunning = true;
+
+ try
+ {
+ // 注意:Tmds.DBus.Protocol 是低层API
+ // 这里使用简化方案,实际生产环境需要完整的DBus信号订阅实现
+ // 当前版本为框架实现,后续可以完善DBus监听逻辑
+
+ while (!ct.IsCancellationRequested && _isRunning)
+ {
+ try
+ {
+ await Task.Delay(1000, ct);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[NotificationBox] Linux通知监听异常: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 处理接收到的通知(供DBus信号处理器调用)
+ ///
+ public void HandleNotification(
+ string appName,
+ uint replacesId,
+ string appIcon,
+ string summary,
+ string body,
+ string[] actions,
+ object hints,
+ int expireTimeout)
+ {
+ try
+ {
+ var notification = new NotificationItem
+ {
+ Id = Guid.NewGuid().ToString(),
+ AppId = appName.ToLowerInvariant().Replace(" ", ""),
+ AppName = appName,
+ Title = summary,
+ Content = StripHtmlTags(body),
+ ReceivedTime = DateTime.Now,
+ AppIconPath = ResolveIconPath(appIcon, appName)
+ };
+
+ _parent.AddNotification(notification);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[NotificationBox] 处理通知失败: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 解析应用图标路径
+ ///
+ private static string? ResolveIconPath(string iconName, string appName)
+ {
+ if (string.IsNullOrEmpty(iconName))
+ {
+ return null;
+ }
+
+ // 如果是绝对路径,直接使用
+ if (File.Exists(iconName))
+ {
+ return iconName;
+ }
+
+ // 尝试从图标主题中查找
+ var iconPaths = new[]
+ {
+ $"/usr/share/icons/hicolor/48x48/apps/{iconName}.png",
+ $"/usr/share/icons/hicolor/64x64/apps/{iconName}.png",
+ $"/usr/share/pixmaps/{iconName}.png",
+ $"/usr/share/pixmaps/{iconName}.svg",
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ $".local/share/icons/{iconName}.png")
+ };
+
+ return iconPaths.FirstOrDefault(File.Exists);
+ }
+
+ ///
+ /// 去除HTML标签(通知内容可能包含HTML)
+ ///
+ private static string StripHtmlTags(string html)
+ {
+ if (string.IsNullOrEmpty(html))
+ {
+ return string.Empty;
+ }
+
+ // 简单的HTML标签去除
+ var result = html;
+ result = System.Text.RegularExpressions.Regex.Replace(result, "<[^>]+>", "");
+ result = result.Replace("<", "<");
+ result = result.Replace(">", ">");
+ result = result.Replace("&", "&");
+ result = result.Replace(""", "\"");
+ return result.Trim();
+ }
+
+ public void Dispose()
+ {
+ _isRunning = false;
+ _cts?.Cancel();
+ _cts?.Dispose();
+ }
+}
diff --git a/LanMountainDesktop/Services/NotificationListenerService.cs b/LanMountainDesktop/Services/NotificationListenerService.cs
new file mode 100644
index 0000000..3eee66a
--- /dev/null
+++ b/LanMountainDesktop/Services/NotificationListenerService.cs
@@ -0,0 +1,201 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Avalonia.Threading;
+using LanMountainDesktop.Models;
+using LanMountainDesktop.PluginSdk;
+
+namespace LanMountainDesktop.Services;
+
+///
+/// 跨平台通知监听服务
+///
+public sealed class NotificationListenerService : IDisposable
+{
+ private readonly List _notifications = [];
+ private readonly object _lock = new();
+ private readonly ISettingsService _settingsService;
+
+ // 平台特定的监听器
+ private LinuxNotificationListener? _linuxListener;
+
+ public event EventHandler? NotificationReceived;
+ public event EventHandler? NotificationRemoved;
+
+ public NotificationListenerService(ISettingsService settingsService)
+ {
+ _settingsService = settingsService;
+ }
+
+ ///
+ /// 初始化并启动监听
+ ///
+ public async Task InitializeAsync()
+ {
+ try
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ // Windows: 使用 UserNotificationListener (需要Windows SDK)
+ // 当前为模拟实现
+ await InitializeWindowsAsync();
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ // Linux: 使用 DBus
+ await InitializeLinuxAsync();
+ }
+ else
+ {
+ // macOS 或其他平台:功能不可用
+ Console.WriteLine("[NotificationBox] 当前平台不支持通知监听");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[NotificationBox] 初始化失败: {ex.Message}");
+ }
+ }
+
+ private async Task InitializeWindowsAsync()
+ {
+ // Windows通知监听实现
+ // 实际项目中需要添加Windows SDK引用并使用UserNotificationListener
+ // 由于需要UWP API,这里使用模拟实现
+ await Task.CompletedTask;
+ Console.WriteLine("[NotificationBox] Windows通知监听已启动(模拟模式)");
+ }
+
+ private async Task InitializeLinuxAsync()
+ {
+ try
+ {
+ _linuxListener = new LinuxNotificationListener(this);
+ var success = await _linuxListener.InitializeAsync();
+
+ if (!success)
+ {
+ Console.WriteLine("[NotificationBox] Linux通知监听初始化失败,可能未运行通知守护进程");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[NotificationBox] Linux通知监听异常: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 添加通知(供平台监听器调用)
+ ///
+ public void AddNotification(NotificationItem notification)
+ {
+ var settings = _settingsService.LoadSnapshot(SettingsScope.App);
+
+ // 检查全局开关
+ if (!settings.NotificationBoxEnabled)
+ return;
+
+ // 检查是否在屏蔽列表中
+ if (settings.NotificationBoxBlockedApps.Contains(notification.AppId, StringComparer.OrdinalIgnoreCase))
+ return;
+
+ lock (_lock)
+ {
+ _notifications.Add(notification);
+ CleanupOldNotifications(settings);
+ }
+
+ // 在UI线程触发事件
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ NotificationReceived?.Invoke(this, notification);
+ });
+ }
+
+ ///
+ /// 移除通知
+ ///
+ public void RemoveNotification(string notificationId)
+ {
+ lock (_lock)
+ {
+ var notification = _notifications.FirstOrDefault(n => n.Id == notificationId);
+ if (notification != null)
+ {
+ _notifications.Remove(notification);
+ }
+ }
+
+ NotificationRemoved?.Invoke(this, notificationId);
+ }
+
+ private void CleanupOldNotifications(AppSettingsSnapshot settings)
+ {
+ // 按数量清理
+ var maxCount = settings.NotificationBoxMaxStoredCount;
+ while (_notifications.Count > maxCount)
+ {
+ _notifications.RemoveAt(0);
+ }
+
+ // 按时间清理
+ var cutoffDate = DateTime.Now.AddDays(-settings.NotificationBoxHistoryRetentionDays);
+ _notifications.RemoveAll(n => n.ReceivedTime < cutoffDate);
+ }
+
+ ///
+ /// 获取所有通知
+ ///
+ public IReadOnlyList GetNotifications()
+ {
+ lock (_lock)
+ {
+ return _notifications.ToList().AsReadOnly();
+ }
+ }
+
+ ///
+ /// 清空所有通知
+ ///
+ public void ClearAll()
+ {
+ lock (_lock)
+ {
+ _notifications.Clear();
+ }
+ }
+
+ ///
+ /// 标记通知为已读
+ ///
+ public void MarkAsRead(string notificationId)
+ {
+ lock (_lock)
+ {
+ var notification = _notifications.FirstOrDefault(n => n.Id == notificationId);
+ if (notification != null)
+ {
+ notification.IsRead = true;
+ }
+ }
+ }
+
+ ///
+ /// 获取未读通知数量
+ ///
+ public int GetUnreadCount()
+ {
+ lock (_lock)
+ {
+ return _notifications.Count(n => !n.IsRead);
+ }
+ }
+
+ public void Dispose()
+ {
+ _linuxListener?.Dispose();
+ ClearAll();
+ }
+}
diff --git a/LanMountainDesktop/ViewModels/NotificationBoxEditorViewModel.cs b/LanMountainDesktop/ViewModels/NotificationBoxEditorViewModel.cs
new file mode 100644
index 0000000..b7aa137
--- /dev/null
+++ b/LanMountainDesktop/ViewModels/NotificationBoxEditorViewModel.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using CommunityToolkit.Mvvm.ComponentModel;
+using LanMountainDesktop.ComponentSystem;
+using LanMountainDesktop.Models;
+
+namespace LanMountainDesktop.ViewModels;
+
+public sealed partial class NotificationBoxEditorViewModel : ViewModelBase
+{
+ private readonly DesktopComponentEditorContext? _context;
+ private bool _isInitializing;
+
+ public NotificationBoxEditorViewModel(DesktopComponentEditorContext? context)
+ {
+ _context = context;
+
+ MaxDisplayCountOptions = new ObservableCollection
+ {
+ new("20", "20条"),
+ new("50", "50条"),
+ new("100", "100条"),
+ new("200", "200条")
+ };
+
+ SortOrderOptions = new ObservableCollection
+ {
+ new("TimeDesc", "最新优先"),
+ new("TimeAsc", "最早优先"),
+ new("AppGroup", "按应用分组")
+ };
+
+ TimeFormatOptions = new ObservableCollection
+ {
+ new("Relative", "相对时间(如:5分钟前)"),
+ new("Absolute", "绝对时间(如:14:30)")
+ };
+
+ LoadSettings();
+ }
+
+ private void LoadSettings()
+ {
+ var snapshot = _context?.ComponentSettingsAccessor.LoadSnapshot()
+ ?? new ComponentSettingsSnapshot();
+
+ _isInitializing = true;
+
+ var countValue = snapshot.NotificationBoxMaxDisplayCount.ToString();
+ SelectedMaxDisplayCount = MaxDisplayCountOptions.FirstOrDefault(o => o.Value == countValue)
+ ?? MaxDisplayCountOptions[1]; // 默认50
+
+ SelectedSortOrder = SortOrderOptions.FirstOrDefault(o => o.Value == snapshot.NotificationBoxSortOrder)
+ ?? SortOrderOptions[0];
+ ShowAppIcon = snapshot.NotificationBoxShowAppIcon;
+ ShowTimestamp = snapshot.NotificationBoxShowTimestamp;
+ SelectedTimeFormat = TimeFormatOptions.FirstOrDefault(o => o.Value == snapshot.NotificationBoxTimeFormat)
+ ?? TimeFormatOptions[0];
+ GroupByApp = snapshot.NotificationBoxGroupByApp;
+ ShowClearButton = snapshot.NotificationBoxShowClearButton;
+
+ _isInitializing = false;
+ }
+
+ private void SaveSettings()
+ {
+ if (_isInitializing || _context == null) return;
+
+ var snapshot = _context.ComponentSettingsAccessor.LoadSnapshot();
+
+ snapshot.NotificationBoxMaxDisplayCount = int.TryParse(SelectedMaxDisplayCount?.Value, out var count) ? count : 50;
+ snapshot.NotificationBoxSortOrder = SelectedSortOrder?.Value ?? "TimeDesc";
+ snapshot.NotificationBoxShowAppIcon = ShowAppIcon;
+ snapshot.NotificationBoxShowTimestamp = ShowTimestamp;
+ snapshot.NotificationBoxTimeFormat = SelectedTimeFormat?.Value ?? "Relative";
+ snapshot.NotificationBoxGroupByApp = GroupByApp;
+ snapshot.NotificationBoxShowClearButton = ShowClearButton;
+
+ _context.ComponentSettingsAccessor.SaveSnapshot(snapshot);
+
+ _context.HostContext.RequestRefresh();
+ }
+
+ [ObservableProperty] private string _descriptionText = "配置此消息盒子组件的显示方式。这些设置仅作用于当前组件实例。";
+ [ObservableProperty] private string _maxDisplayCountLabel = "最大显示数量";
+ [ObservableProperty] private string _maxDisplayCountDescription = "组件中最多显示的通知条数";
+ [ObservableProperty] private string _sortOrderLabel = "排序方式";
+ [ObservableProperty] private string _displayOptionsLabel = "显示选项";
+ [ObservableProperty] private string _showAppIconLabel = "显示应用图标";
+ [ObservableProperty] private string _showTimestampLabel = "显示时间戳";
+ [ObservableProperty] private string _groupByAppLabel = "按应用分组显示";
+ [ObservableProperty] private string _showClearButtonLabel = "显示清空按钮";
+ [ObservableProperty] private string _timeFormatLabel = "时间格式";
+
+ [ObservableProperty] private SelectionOption? _selectedMaxDisplayCount;
+ [ObservableProperty] private SelectionOption? _selectedSortOrder;
+ [ObservableProperty] private bool _showAppIcon = true;
+ [ObservableProperty] private bool _showTimestamp = true;
+ [ObservableProperty] private SelectionOption? _selectedTimeFormat;
+ [ObservableProperty] private bool _groupByApp = false;
+ [ObservableProperty] private bool _showClearButton = true;
+
+ public ObservableCollection MaxDisplayCountOptions { get; }
+ public ObservableCollection SortOrderOptions { get; }
+ public ObservableCollection TimeFormatOptions { get; }
+
+ partial void OnSelectedMaxDisplayCountChanged(SelectionOption? value) => SaveSettings();
+ partial void OnSelectedSortOrderChanged(SelectionOption? value) => SaveSettings();
+ partial void OnShowAppIconChanged(bool value) => SaveSettings();
+ partial void OnShowTimestampChanged(bool value) => SaveSettings();
+ partial void OnSelectedTimeFormatChanged(SelectionOption? value) => SaveSettings();
+ partial void OnGroupByAppChanged(bool value) => SaveSettings();
+ partial void OnShowClearButtonChanged(bool value) => SaveSettings();
+}
diff --git a/LanMountainDesktop/Views/ComponentEditors/NotificationBoxComponentEditor.axaml b/LanMountainDesktop/Views/ComponentEditors/NotificationBoxComponentEditor.axaml
new file mode 100644
index 0000000..52694b9
--- /dev/null
+++ b/LanMountainDesktop/Views/ComponentEditors/NotificationBoxComponentEditor.axaml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/ComponentEditors/NotificationBoxComponentEditor.axaml.cs b/LanMountainDesktop/Views/ComponentEditors/NotificationBoxComponentEditor.axaml.cs
new file mode 100644
index 0000000..8112330
--- /dev/null
+++ b/LanMountainDesktop/Views/ComponentEditors/NotificationBoxComponentEditor.axaml.cs
@@ -0,0 +1,15 @@
+using Avalonia.Controls;
+using LanMountainDesktop.ComponentSystem;
+using LanMountainDesktop.ViewModels;
+
+namespace LanMountainDesktop.Views.ComponentEditors;
+
+public partial class NotificationBoxComponentEditor : ComponentEditorViewBase
+{
+ public NotificationBoxComponentEditor(DesktopComponentEditorContext? context)
+ : base(context)
+ {
+ InitializeComponent();
+ DataContext = new NotificationBoxEditorViewModel(context);
+ }
+}
diff --git a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs
index 885d521..f2f9e94 100644
--- a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs
+++ b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs
@@ -479,7 +479,11 @@ public sealed class DesktopComponentRuntimeRegistry
new DesktopComponentRuntimeRegistration(
BuiltInComponentIds.DesktopFileManager,
"component.file_manager",
- () => new FileManagerWidget())
+ () => new FileManagerWidget()),
+ new DesktopComponentRuntimeRegistration(
+ BuiltInComponentIds.DesktopNotificationBox,
+ "component.notification_box",
+ () => new NotificationBoxWidget())
];
}
diff --git a/LanMountainDesktop/Views/Components/NotificationBoxWidget.axaml b/LanMountainDesktop/Views/Components/NotificationBoxWidget.axaml
new file mode 100644
index 0000000..7f03a6f
--- /dev/null
+++ b/LanMountainDesktop/Views/Components/NotificationBoxWidget.axaml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/Components/NotificationBoxWidget.axaml.cs b/LanMountainDesktop/Views/Components/NotificationBoxWidget.axaml.cs
new file mode 100644
index 0000000..fbf9851
--- /dev/null
+++ b/LanMountainDesktop/Views/Components/NotificationBoxWidget.axaml.cs
@@ -0,0 +1,529 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Styling;
+using Avalonia.Threading;
+using LanMountainDesktop.ComponentSystem;
+using LanMountainDesktop.Models;
+using LanMountainDesktop.PluginSdk;
+using LanMountainDesktop.Services;
+using LanMountainDesktop.Services.Settings;
+
+namespace LanMountainDesktop.Views.Components;
+
+public partial class NotificationBoxWidget : UserControl,
+ IDesktopComponentWidget,
+ IComponentSettingsContextAware,
+ IComponentRuntimeContextAware
+{
+ private readonly List _notificationControls = [];
+ private NotificationListenerService? _notificationService;
+ private IComponentInstanceSettingsStore _componentSettings = null!;
+ private ISettingsService _appSettingsService = null!;
+ private AppSettingsSnapshot _appSettings = new();
+ private ComponentSettingsSnapshot _componentSettingsSnapshot = new();
+ private bool _isAttached;
+ private bool _isPrivacyMode;
+ private bool _isNightVisual;
+ private double _currentCellSize = 48d;
+
+ public NotificationBoxWidget()
+ {
+ InitializeComponent();
+
+ AttachedToVisualTree += OnAttachedToVisualTree;
+ DetachedFromVisualTree += OnDetachedFromVisualTree;
+ ActualThemeVariantChanged += OnActualThemeVariantChanged;
+
+ PointerPressed += (_, e) =>
+ {
+ if (e.Source == NotificationListPanel)
+ {
+ ClearSelection();
+ }
+ };
+ }
+
+ public void ApplyCellSize(double cellSize)
+ {
+ _currentCellSize = Math.Max(1, cellSize);
+ UpdateAdaptiveLayout();
+ }
+
+ public void SetComponentSettingsContext(DesktopComponentSettingsContext context)
+ {
+ _componentSettings = context.ComponentSettingsStore;
+ LoadSettings();
+ RefreshUI();
+ }
+
+ public void SetComponentRuntimeContext(DesktopComponentRuntimeContext context)
+ {
+ _notificationService = NotificationListenerServiceProvider.GetOrCreate(_appSettingsService);
+ if (_notificationService != null)
+ {
+ _notificationService.NotificationReceived += OnNotificationReceived;
+ _notificationService.NotificationRemoved += OnNotificationRemoved;
+ }
+ }
+
+ private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
+ {
+ _isAttached = true;
+ _isNightVisual = ResolveNightMode();
+ LoadSettings();
+ RefreshUI();
+ UpdateAdaptiveLayout();
+ }
+
+ private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
+ {
+ _isAttached = false;
+
+ if (_notificationService != null)
+ {
+ _notificationService.NotificationReceived -= OnNotificationReceived;
+ _notificationService.NotificationRemoved -= OnNotificationRemoved;
+ }
+ }
+
+ private void OnActualThemeVariantChanged(object? sender, EventArgs e)
+ {
+ _isNightVisual = ResolveNightMode();
+ UpdateAdaptiveLayout();
+ }
+
+ private bool ResolveNightMode()
+ {
+ if (ActualThemeVariant == ThemeVariant.Dark)
+ {
+ return true;
+ }
+
+ if (ActualThemeVariant == ThemeVariant.Light)
+ {
+ return false;
+ }
+
+ if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) &&
+ value is ISolidColorBrush brush)
+ {
+ return CalculateRelativeLuminance(brush.Color) < 0.45;
+ }
+
+ return true;
+ }
+
+ private static double CalculateRelativeLuminance(Color color)
+ {
+ static double ToLinear(double channel)
+ {
+ return channel <= 0.03928
+ ? channel / 12.92
+ : Math.Pow((channel + 0.055) / 1.055, 2.4);
+ }
+
+ var r = ToLinear(color.R / 255d);
+ var g = ToLinear(color.G / 255d);
+ var b = ToLinear(color.B / 255d);
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
+ }
+
+ private void LoadSettings()
+ {
+ var appSettingsFacade = HostSettingsFacadeProvider.GetOrCreate();
+ _appSettingsService = appSettingsFacade.Settings;
+ _appSettings = _appSettingsService.LoadSnapshot(SettingsScope.App);
+ _isPrivacyMode = _appSettings.NotificationBoxPrivacyMode;
+
+ _componentSettingsSnapshot = _componentSettings?.Load()
+ ?? new ComponentSettingsSnapshot();
+ }
+
+ private void RefreshUI()
+ {
+ if (!_isAttached) return;
+
+ var hasNotifications = _notificationService?.GetNotifications().Count > 0;
+ PrivacyOverlay.IsVisible = _isPrivacyMode && hasNotifications && _notificationService?.GetUnreadCount() > 0;
+ NotificationListPanel.IsVisible = !PrivacyOverlay.IsVisible;
+
+ var unreadCount = _notificationService?.GetUnreadCount() ?? 0;
+ UnreadBadge.IsVisible = unreadCount > 0;
+ UnreadCountText.Text = unreadCount.ToString();
+
+ ClearButton.IsVisible = _componentSettingsSnapshot.NotificationBoxShowClearButton
+ && hasNotifications;
+
+ UpdateStatusText();
+ RenderNotifications();
+ }
+
+ private void RenderNotifications()
+ {
+ NotificationListPanel.Children.Clear();
+ _notificationControls.Clear();
+
+ if (_notificationService == null)
+ {
+ EmptyStateText.IsVisible = true;
+ EmptyStateText.Text = "通知服务未启动";
+ return;
+ }
+
+ var notifications = _notificationService.GetNotifications();
+
+ if (notifications.Count == 0)
+ {
+ EmptyStateText.IsVisible = true;
+ EmptyStateText.Text = "暂无通知";
+ return;
+ }
+
+ EmptyStateText.IsVisible = false;
+
+ notifications = ApplySorting(notifications);
+
+ var maxCount = _componentSettingsSnapshot.NotificationBoxMaxDisplayCount;
+ notifications = notifications.Take(maxCount).ToList();
+
+ if (_componentSettingsSnapshot.NotificationBoxGroupByApp)
+ {
+ RenderGroupedNotifications(notifications);
+ }
+ else
+ {
+ RenderFlatNotifications(notifications);
+ }
+ }
+
+ private IReadOnlyList ApplySorting(IReadOnlyList notifications)
+ {
+ return _componentSettingsSnapshot.NotificationBoxSortOrder switch
+ {
+ "TimeAsc" => notifications.OrderBy(n => n.ReceivedTime).ToList(),
+ "AppGroup" => notifications.OrderBy(n => n.AppName).ThenByDescending(n => n.ReceivedTime).ToList(),
+ _ => notifications.OrderByDescending(n => n.ReceivedTime).ToList()
+ };
+ }
+
+ private void RenderFlatNotifications(IReadOnlyList notifications)
+ {
+ foreach (var notification in notifications)
+ {
+ var control = CreateNotificationControl(notification);
+ NotificationListPanel.Children.Add(control);
+ _notificationControls.Add(control);
+ }
+ }
+
+ private void RenderGroupedNotifications(IReadOnlyList notifications)
+ {
+ var grouped = notifications.GroupBy(n => n.AppName).ToList();
+
+ foreach (var group in grouped)
+ {
+ var groupHeader = new TextBlock
+ {
+ Text = group.Key,
+ FontWeight = FontWeight.SemiBold,
+ FontSize = 11,
+ Foreground = new SolidColorBrush(Color.Parse("#8B95A5")),
+ Margin = new Thickness(0, 6, 0, 3)
+ };
+ NotificationListPanel.Children.Add(groupHeader);
+
+ foreach (var notification in group)
+ {
+ var control = CreateNotificationControl(notification);
+ NotificationListPanel.Children.Add(control);
+ _notificationControls.Add(control);
+ }
+ }
+ }
+
+ private NotificationItemControl CreateNotificationControl(NotificationItem notification)
+ {
+ var control = new NotificationItemControl(notification, _componentSettingsSnapshot, _isNightVisual);
+ control.Clicked += OnNotificationClicked;
+ control.MarkAsRead += OnMarkAsRead;
+ return control;
+ }
+
+ private void OnNotificationReceived(object? sender, NotificationItem notification)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ if (!_isAttached) return;
+ RefreshUI();
+ });
+ }
+
+ private void OnNotificationRemoved(object? sender, string notificationId)
+ {
+ Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ if (!_isAttached) return;
+ RefreshUI();
+ });
+ }
+
+ private void OnNotificationClicked(object? sender, NotificationItem notification)
+ {
+ }
+
+ private void OnMarkAsRead(object? sender, NotificationItem notification)
+ {
+ _notificationService?.MarkAsRead(notification.Id);
+ RefreshUI();
+ }
+
+ private void OnClearButtonClick(object? sender, RoutedEventArgs e)
+ {
+ _notificationService?.ClearAll();
+ RefreshUI();
+ e.Handled = true;
+ }
+
+ private void UpdateStatusText()
+ {
+ var total = _notificationService?.GetNotifications().Count ?? 0;
+ var max = _componentSettingsSnapshot.NotificationBoxMaxDisplayCount;
+ StatusTextBlock.Text = $"共 {total} 条" + (total > max ? $"(显{max})" : "");
+ }
+
+ private void ClearSelection()
+ {
+ foreach (var control in _notificationControls)
+ {
+ control.IsSelected = false;
+ }
+ }
+
+ private void UpdateAdaptiveLayout()
+ {
+ var scale = Math.Clamp(_currentCellSize / 48.0, 0.7, 1.8);
+ var fontScale = Math.Clamp(scale, 0.8, 1.4);
+
+ var cornerRadius = ResolveUnifiedMainRadiusValue();
+ RootBorder.CornerRadius = new CornerRadius(cornerRadius);
+ CardBorder.CornerRadius = new CornerRadius(cornerRadius);
+
+ CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#FCFCFD"));
+
+ HeaderTextBlock.FontSize = 15 * fontScale;
+ HeaderIcon.FontSize = 16 * fontScale;
+ UnreadCountText.FontSize = 11 * fontScale;
+ EmptyStateText.FontSize = 13 * fontScale;
+ StatusTextBlock.FontSize = 11 * fontScale;
+
+ var padding = Math.Clamp(12 * scale, 8, 20);
+ var verticalPadding = Math.Clamp(10 * scale, 6, 16);
+ CardBorder.Padding = new Thickness(padding, verticalPadding);
+
+ foreach (var control in _notificationControls)
+ {
+ control.UpdateTheme(_isNightVisual, fontScale);
+ }
+ }
+
+ private static double ResolveUnifiedMainRadiusValue() =>
+ HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
+}
+
+public class NotificationItemControl : Border
+{
+ private readonly NotificationItem _item;
+ private readonly ComponentSettingsSnapshot _settings;
+ private bool _isPointerPressed;
+ private Point _pointerPressedPosition;
+ private bool _isNightVisual;
+
+ public NotificationItemControl(NotificationItem item, ComponentSettingsSnapshot settings, bool isNightVisual)
+ {
+ _item = item;
+ _settings = settings;
+ _isNightVisual = isNightVisual;
+
+ Background = _item.IsRead
+ ? new SolidColorBrush(isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#F5F5F5"))
+ : new SolidColorBrush(isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#FFFFFF"));
+ CornerRadius = new CornerRadius(6);
+ Padding = new Thickness(10, 6);
+ Cursor = new Cursor(StandardCursorType.Hand);
+ BorderBrush = _item.IsRead
+ ? new SolidColorBrush(Colors.Transparent)
+ : new SolidColorBrush(Color.Parse("#E24B2D"));
+ BorderThickness = _item.IsRead ? new Thickness(0) : new Thickness(2, 0, 0, 0);
+
+ BuildUI();
+
+ PointerPressed += OnPointerPressed;
+ PointerReleased += OnPointerReleased;
+ }
+
+ private void BuildUI()
+ {
+ var grid = new Grid { ColumnDefinitions = ColumnDefinitions.Parse("Auto,*,Auto") };
+
+ if (_settings.NotificationBoxShowAppIcon)
+ {
+ var iconBorder = new Border
+ {
+ Width = 28,
+ Height = 28,
+ CornerRadius = new CornerRadius(4),
+ Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#4D5560") : Color.Parse("#E8EAED")),
+ Margin = new Thickness(0, 0, 8, 0)
+ };
+ var iconText = new TextBlock
+ {
+ Text = _item.AppName.Length > 0 ? _item.AppName[0].ToString() : "?",
+ HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
+ VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
+ FontSize = 12,
+ FontWeight = FontWeight.SemiBold,
+ Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327"))
+ };
+ iconBorder.Child = iconText;
+ grid.Children.Add(iconBorder);
+ }
+
+ var contentPanel = new StackPanel { Spacing = 1 };
+ Grid.SetColumn(contentPanel, 1);
+
+ var titleBlock = new TextBlock
+ {
+ Text = _item.Title,
+ FontWeight = FontWeight.SemiBold,
+ FontSize = 12,
+ TextTrimming = TextTrimming.CharacterEllipsis,
+ MaxLines = 1,
+ Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327"))
+ };
+
+ var contentBlock = new TextBlock
+ {
+ Text = _item.Content,
+ FontSize = 11,
+ Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671")),
+ TextTrimming = TextTrimming.CharacterEllipsis,
+ MaxLines = 2,
+ TextWrapping = TextWrapping.Wrap
+ };
+
+ contentPanel.Children.Add(titleBlock);
+ if (!string.IsNullOrWhiteSpace(_item.Content))
+ {
+ contentPanel.Children.Add(contentBlock);
+ }
+ grid.Children.Add(contentPanel);
+
+ if (_settings.NotificationBoxShowTimestamp)
+ {
+ var timeText = _settings.NotificationBoxTimeFormat == "Relative"
+ ? GetRelativeTime(_item.ReceivedTime)
+ : _item.ReceivedTime.ToString("HH:mm");
+
+ var timeBlock = new TextBlock
+ {
+ Text = timeText,
+ FontSize = 10,
+ Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#8B95A5") : Color.Parse("#8B95A5")),
+ VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top,
+ Margin = new Thickness(6, 0, 0, 0)
+ };
+ Grid.SetColumn(timeBlock, 2);
+ grid.Children.Add(timeBlock);
+ }
+
+ Child = grid;
+ }
+
+ public void UpdateTheme(bool isNightVisual, double fontScale)
+ {
+ _isNightVisual = isNightVisual;
+ Background = _item.IsRead
+ ? new SolidColorBrush(isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#F5F5F5"))
+ : new SolidColorBrush(isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#FFFFFF"));
+
+ if (Child is Grid grid)
+ {
+ foreach (var child in grid.Children)
+ {
+ if (child is StackPanel panel)
+ {
+ foreach (var textBlock in panel.Children.OfType())
+ {
+ textBlock.FontSize *= fontScale;
+ }
+ }
+ }
+ }
+ }
+
+ private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
+ {
+ _isPointerPressed = true;
+ _pointerPressedPosition = e.GetPosition(this);
+ IsSelected = true;
+ e.Handled = true;
+ }
+ }
+
+ private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
+ {
+ if (!_isPointerPressed) return;
+
+ _isPointerPressed = false;
+ var releasePosition = e.GetPosition(this);
+ var distance = Math.Sqrt(
+ Math.Pow(releasePosition.X - _pointerPressedPosition.X, 2) +
+ Math.Pow(releasePosition.Y - _pointerPressedPosition.Y, 2));
+
+ if (distance < 5)
+ {
+ Clicked?.Invoke(this, _item);
+ MarkAsRead?.Invoke(this, _item);
+ }
+
+ e.Handled = true;
+ }
+
+ private static string GetRelativeTime(DateTime time)
+ {
+ var diff = DateTime.Now - time;
+
+ if (diff.TotalMinutes < 1) return "刚刚";
+ if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}分前";
+ if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}小时前";
+ return $"{(int)diff.TotalDays}天前";
+ }
+
+ public bool IsSelected { get; set; }
+
+ public event EventHandler? Clicked;
+ public event EventHandler? MarkAsRead;
+}
+
+public static class NotificationListenerServiceProvider
+{
+ private static NotificationListenerService? _instance;
+
+ public static NotificationListenerService GetOrCreate(ISettingsService settingsService)
+ {
+ if (_instance == null)
+ {
+ _instance = new NotificationListenerService(settingsService);
+ _instance.InitializeAsync().ConfigureAwait(false);
+ }
+ return _instance;
+ }
+}