diff --git a/EmoTracker.Data/Settings/ApplicationColors.cs b/EmoTracker.Data/Settings/ApplicationColors.cs index 682a7c0..6b75409 100644 --- a/EmoTracker.Data/Settings/ApplicationColors.cs +++ b/EmoTracker.Data/Settings/ApplicationColors.cs @@ -98,6 +98,36 @@ public string Status_Generic_Active #endregion + #region -- Notification Colors -- + + string mNotification_Message = "#41a054"; + string mNotification_Celebration = "CornflowerBlue"; + string mNotification_Warning = "#c09d35"; + string mNotification_Error = "#c03535"; + + public string Notification_Message + { + get { return mNotification_Message; } + set { SetProperty(ref mNotification_Message, value); } + } + public string Notification_Celebration + { + get { return mNotification_Celebration; } + set { SetProperty(ref mNotification_Celebration, value); } + } + public string Notification_Warning + { + get { return mNotification_Warning; } + set { SetProperty(ref mNotification_Warning, value); } + } + public string Notification_Error + { + get { return mNotification_Error; } + set { SetProperty(ref mNotification_Error, value); } + } + + #endregion + #region -- Miscellaneous -- string mMap_LocationNoteBadgeBackground = "#35e0b5"; @@ -132,6 +162,11 @@ public void ResetColors() Status_Generic_Active = "#35e0b5"; Map_LocationNoteBadgeBackground = "#35e0b5"; + + Notification_Message = "#41a054"; + Notification_Celebration = "CornflowerBlue"; + Notification_Warning = "#c09d35"; + Notification_Error = "#c03535"; } public void LoadColors() @@ -161,6 +196,11 @@ public void LoadColors() Status_Generic_Error = root.GetValue("status_generic_error", Status_Generic_Error); Map_LocationNoteBadgeBackground = root.GetValue("map_location_has_note_badge_background", Map_LocationNoteBadgeBackground); + + Notification_Message = root.GetValue("notification_message", Notification_Message); + Notification_Celebration = root.GetValue("notification_celebration", Notification_Celebration); + Notification_Warning = root.GetValue("notification_warning", Notification_Warning); + Notification_Error = root.GetValue("notification_error", Notification_Error); } } catch diff --git a/EmoTracker.UI/Converters/BrushConverters.cs b/EmoTracker.UI/Converters/BrushConverters.cs index c05794a..1b0cd83 100644 --- a/EmoTracker.UI/Converters/BrushConverters.cs +++ b/EmoTracker.UI/Converters/BrushConverters.cs @@ -1,6 +1,7 @@ #nullable enable annotations using EmoTracker.Core; using EmoTracker.Data.Locations; +using EmoTracker.Data.Scripting; using EmoTracker.Data.Settings; using System; using System.Collections.Generic; @@ -98,6 +99,32 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu => throw new NotSupportedException(); } + /// + /// Converts a to its matching accent + /// for the notification left-border stripe. Colors are resolved live from + /// so user configuration is respected. + /// + public class NotificationTypeToBrushConverter : Singleton, IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var c = ApplicationColors.Instance; + string colorStr = value is NotificationType t ? t switch + { + NotificationType.Celebration => c.Notification_Celebration, + NotificationType.Warning => c.Notification_Warning, + NotificationType.Error => c.Notification_Error, + _ => c.Notification_Message, + } : c.Notification_Message; + + try { return Brush.Parse(colorStr); } + catch { return Brush.Parse(c.Notification_Message); } + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotSupportedException(); + } + /// /// Multi-value converter for the Package Manager button foreground color. /// Replicates WPF DataTrigger priority: !AnyPackagesInstalled → Active, diff --git a/EmoTracker/ApplicationModel.cs b/EmoTracker/ApplicationModel.cs index 17a8007..df82804 100644 --- a/EmoTracker/ApplicationModel.cs +++ b/EmoTracker/ApplicationModel.cs @@ -1281,6 +1281,15 @@ private void NotificationExpirationTimer_Tick(object sender, EventArgs e) } } + private void OnNotificationForceExpired(object sender, EventArgs e) + { + if (sender is Notification n) + { + n.ForceExpired -= OnNotificationForceExpired; + mNotifications.Remove(n); + } + } + private void Notifications_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { NotifyPropertyChanged("HasPendingNotifications"); @@ -1305,6 +1314,7 @@ public void PushMarkdownNotification(NotificationType type, string markdown, int mPreviousNotifications.RemoveAt(9); } + notification.ForceExpired += OnNotificationForceExpired; mPreviousNotifications.Insert(0, notification); mNotifications.Insert(0, notification); }); diff --git a/EmoTracker/Extensions/McpServer/McpServerExtension.cs b/EmoTracker/Extensions/McpServer/McpServerExtension.cs index 5f480e3..c3be07f 100644 --- a/EmoTracker/Extensions/McpServer/McpServerExtension.cs +++ b/EmoTracker/Extensions/McpServer/McpServerExtension.cs @@ -117,6 +117,7 @@ private async Task StartServerAsync() .WithTools() .WithTools() .WithTools() + .WithTools() .WithHttpTransport(); mApp = builder.Build(); diff --git a/EmoTracker/Extensions/McpServer/Tools/NotificationTools.cs b/EmoTracker/Extensions/McpServer/Tools/NotificationTools.cs new file mode 100644 index 0000000..01f8a99 --- /dev/null +++ b/EmoTracker/Extensions/McpServer/Tools/NotificationTools.cs @@ -0,0 +1,38 @@ +using Avalonia.Threading; +using EmoTracker.Data.Scripting; +using ModelContextProtocol.Server; +using System; +using System.ComponentModel; +using System.Text.Json; +using System.Threading.Tasks; + +namespace EmoTracker.Extensions.McpServer.Tools +{ + [McpServerToolType] + public class NotificationTools + { + [McpServerTool(Name = "push_notification")] + [Description("Push a markdown notification to the tracker UI. Type must be one of: Message, Celebration, Warning, Error. Timeout is in milliseconds (-1 = 10s default, 0 = never expires).")] + public static async Task PushNotification( + [Description("Notification type: Message, Celebration, Warning, or Error")] string type, + [Description("Markdown content for the notification")] string markdown, + [Description("Timeout in milliseconds (-1 for default 10 seconds, 0 for no expiry)")] int timeout = -1) + { + return await Dispatcher.UIThread.InvokeAsync(() => + { + try + { + if (!Enum.TryParse(type, ignoreCase: true, out var notifType)) + return JsonSerializer.Serialize(new { success = false, error = $"Invalid type '{type}'. Must be: Message, Celebration, Warning, or Error." }); + + ApplicationModel.Instance.PushMarkdownNotification(notifType, markdown, timeout); + return JsonSerializer.Serialize(new { success = true, type = notifType.ToString(), markdown }); + } + catch (Exception ex) + { + return JsonSerializer.Serialize(new { success = false, error = ex.Message }); + } + }); + } + } +} diff --git a/EmoTracker/MainWindow.axaml b/EmoTracker/MainWindow.axaml index f1ad373..689890a 100644 --- a/EmoTracker/MainWindow.axaml +++ b/EmoTracker/MainWindow.axaml @@ -51,15 +51,26 @@ Margin="20" VerticalAlignment="Top"> - - + diff --git a/EmoTracker/Notifications/Notification.cs b/EmoTracker/Notifications/Notification.cs index 80013fa..eefc823 100644 --- a/EmoTracker/Notifications/Notification.cs +++ b/EmoTracker/Notifications/Notification.cs @@ -44,9 +44,13 @@ protected Notification(int timeout) mForceExpireCommand = new DelegateCommand(ForceExpireNotification); } + public event EventHandler ForceExpired; + private void ForceExpireNotification(object obj) { + ExpirationTime = DateTime.Now; Expired = true; + ForceExpired?.Invoke(this, EventArgs.Empty); } } }