Skip to content

Commit b3b7dee

Browse files
emosaruEmoSaruclaude
authored
Add notification system: type-specific colors, drop shadow, click-to-dismiss, MCP tool (#53)
- Render notifications with a drop shadow matching layout element shadow config - Apply type-specific left-border accent colors (Message=green, Celebration=cornflower blue, Warning=amber, Error=red) - Expose notification colors in ApplicationColors with JSON-configurable keys (notification_message/celebration/warning/error) - Add NotificationTypeToBrushConverter that reads live from ApplicationColors.Instance - Fix click-to-dismiss: replace Opacity=0 button overlay (Avalonia hit-test dead) with full-card Button; add ForceExpired event for immediate removal bypassing the timer - Add push_notification MCP tool for all four NotificationTypes Co-authored-by: EmoSaru <emosaru@emosaru.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 34e1f2d commit b3b7dee

7 files changed

Lines changed: 139 additions & 8 deletions

File tree

EmoTracker.Data/Settings/ApplicationColors.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,36 @@ public string Status_Generic_Active
9898

9999
#endregion
100100

101+
#region -- Notification Colors --
102+
103+
string mNotification_Message = "#41a054";
104+
string mNotification_Celebration = "CornflowerBlue";
105+
string mNotification_Warning = "#c09d35";
106+
string mNotification_Error = "#c03535";
107+
108+
public string Notification_Message
109+
{
110+
get { return mNotification_Message; }
111+
set { SetProperty(ref mNotification_Message, value); }
112+
}
113+
public string Notification_Celebration
114+
{
115+
get { return mNotification_Celebration; }
116+
set { SetProperty(ref mNotification_Celebration, value); }
117+
}
118+
public string Notification_Warning
119+
{
120+
get { return mNotification_Warning; }
121+
set { SetProperty(ref mNotification_Warning, value); }
122+
}
123+
public string Notification_Error
124+
{
125+
get { return mNotification_Error; }
126+
set { SetProperty(ref mNotification_Error, value); }
127+
}
128+
129+
#endregion
130+
101131
#region -- Miscellaneous --
102132

103133
string mMap_LocationNoteBadgeBackground = "#35e0b5";
@@ -132,6 +162,11 @@ public void ResetColors()
132162
Status_Generic_Active = "#35e0b5";
133163

134164
Map_LocationNoteBadgeBackground = "#35e0b5";
165+
166+
Notification_Message = "#41a054";
167+
Notification_Celebration = "CornflowerBlue";
168+
Notification_Warning = "#c09d35";
169+
Notification_Error = "#c03535";
135170
}
136171

137172
public void LoadColors()
@@ -161,6 +196,11 @@ public void LoadColors()
161196
Status_Generic_Error = root.GetValue<string>("status_generic_error", Status_Generic_Error);
162197

163198
Map_LocationNoteBadgeBackground = root.GetValue<string>("map_location_has_note_badge_background", Map_LocationNoteBadgeBackground);
199+
200+
Notification_Message = root.GetValue<string>("notification_message", Notification_Message);
201+
Notification_Celebration = root.GetValue<string>("notification_celebration", Notification_Celebration);
202+
Notification_Warning = root.GetValue<string>("notification_warning", Notification_Warning);
203+
Notification_Error = root.GetValue<string>("notification_error", Notification_Error);
164204
}
165205
}
166206
catch

EmoTracker.UI/Converters/BrushConverters.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#nullable enable annotations
22
using EmoTracker.Core;
33
using EmoTracker.Data.Locations;
4+
using EmoTracker.Data.Scripting;
45
using EmoTracker.Data.Settings;
56
using System;
67
using System.Collections.Generic;
@@ -98,6 +99,32 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu
9899
=> throw new NotSupportedException();
99100
}
100101

102+
/// <summary>
103+
/// Converts a <see cref="NotificationType"/> to its matching accent <see cref="IBrush"/>
104+
/// for the notification left-border stripe. Colors are resolved live from
105+
/// <see cref="ApplicationColors.Instance"/> so user configuration is respected.
106+
/// </summary>
107+
public class NotificationTypeToBrushConverter : Singleton<NotificationTypeToBrushConverter>, IValueConverter
108+
{
109+
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
110+
{
111+
var c = ApplicationColors.Instance;
112+
string colorStr = value is NotificationType t ? t switch
113+
{
114+
NotificationType.Celebration => c.Notification_Celebration,
115+
NotificationType.Warning => c.Notification_Warning,
116+
NotificationType.Error => c.Notification_Error,
117+
_ => c.Notification_Message,
118+
} : c.Notification_Message;
119+
120+
try { return Brush.Parse(colorStr); }
121+
catch { return Brush.Parse(c.Notification_Message); }
122+
}
123+
124+
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
125+
=> throw new NotSupportedException();
126+
}
127+
101128
/// <summary>
102129
/// Multi-value converter for the Package Manager button foreground color.
103130
/// Replicates WPF DataTrigger priority: !AnyPackagesInstalled → Active,

EmoTracker/ApplicationModel.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,6 +1281,15 @@ private void NotificationExpirationTimer_Tick(object sender, EventArgs e)
12811281
}
12821282
}
12831283

1284+
private void OnNotificationForceExpired(object sender, EventArgs e)
1285+
{
1286+
if (sender is Notification n)
1287+
{
1288+
n.ForceExpired -= OnNotificationForceExpired;
1289+
mNotifications.Remove(n);
1290+
}
1291+
}
1292+
12841293
private void Notifications_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
12851294
{
12861295
NotifyPropertyChanged("HasPendingNotifications");
@@ -1305,6 +1314,7 @@ public void PushMarkdownNotification(NotificationType type, string markdown, int
13051314
mPreviousNotifications.RemoveAt(9);
13061315
}
13071316

1317+
notification.ForceExpired += OnNotificationForceExpired;
13081318
mPreviousNotifications.Insert(0, notification);
13091319
mNotifications.Insert(0, notification);
13101320
});

EmoTracker/Extensions/McpServer/McpServerExtension.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ private async Task StartServerAsync()
117117
.WithTools<Tools.ExtensionTools>()
118118
.WithTools<Tools.PackageTools>()
119119
.WithTools<Tools.ImageCacheTools>()
120+
.WithTools<Tools.NotificationTools>()
120121
.WithHttpTransport();
121122

122123
mApp = builder.Build();
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using Avalonia.Threading;
2+
using EmoTracker.Data.Scripting;
3+
using ModelContextProtocol.Server;
4+
using System;
5+
using System.ComponentModel;
6+
using System.Text.Json;
7+
using System.Threading.Tasks;
8+
9+
namespace EmoTracker.Extensions.McpServer.Tools
10+
{
11+
[McpServerToolType]
12+
public class NotificationTools
13+
{
14+
[McpServerTool(Name = "push_notification")]
15+
[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).")]
16+
public static async Task<string> PushNotification(
17+
[Description("Notification type: Message, Celebration, Warning, or Error")] string type,
18+
[Description("Markdown content for the notification")] string markdown,
19+
[Description("Timeout in milliseconds (-1 for default 10 seconds, 0 for no expiry)")] int timeout = -1)
20+
{
21+
return await Dispatcher.UIThread.InvokeAsync(() =>
22+
{
23+
try
24+
{
25+
if (!Enum.TryParse<NotificationType>(type, ignoreCase: true, out var notifType))
26+
return JsonSerializer.Serialize(new { success = false, error = $"Invalid type '{type}'. Must be: Message, Celebration, Warning, or Error." });
27+
28+
ApplicationModel.Instance.PushMarkdownNotification(notifType, markdown, timeout);
29+
return JsonSerializer.Serialize(new { success = true, type = notifType.ToString(), markdown });
30+
}
31+
catch (Exception ex)
32+
{
33+
return JsonSerializer.Serialize(new { success = false, error = ex.Message });
34+
}
35+
});
36+
}
37+
}
38+
}

EmoTracker/MainWindow.axaml

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,26 @@
5151
Margin="20" VerticalAlignment="Top">
5252
<ItemsControl.ItemTemplate>
5353
<DataTemplate>
54-
<Border Background="#d8212121" BorderThickness="8,0,0,0"
55-
BorderBrush="#41a054" Margin="0,0,0,10">
56-
<Grid Background="Transparent">
54+
<Button Command="{Binding ForceExpireCommand}"
55+
Padding="0" Background="Transparent" BorderThickness="0"
56+
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
57+
<Button.Styles>
58+
<Style Selector="Button:pointerover /template/ ContentPresenter">
59+
<Setter Property="Background" Value="Transparent"/>
60+
</Style>
61+
<Style Selector="Button:pressed /template/ ContentPresenter">
62+
<Setter Property="Background" Value="Transparent"/>
63+
</Style>
64+
</Button.Styles>
65+
<Border Background="#d8212121" BorderThickness="8,0,0,0"
66+
BorderBrush="{Binding Type, Converter={x:Static converters:NotificationTypeToBrushConverter.Instance}}"
67+
Margin="0,0,0,10">
68+
<Border.Effect>
69+
<DropShadowDirectionEffect BlurRadius="15" ShadowDepth="0" Opacity="0.8" Color="Black"/>
70+
</Border.Effect>
5771
<controls:MarkdownViewer Markdown="{Binding Markdown}" Margin="10,7"/>
58-
<Button Background="Transparent" BorderThickness="0"
59-
Command="{Binding ForceExpireCommand}"
60-
Opacity="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
61-
</Grid>
62-
</Border>
72+
</Border>
73+
</Button>
6374
</DataTemplate>
6475
</ItemsControl.ItemTemplate>
6576
</ItemsControl>

EmoTracker/Notifications/Notification.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,13 @@ protected Notification(int timeout)
4444

4545
mForceExpireCommand = new DelegateCommand(ForceExpireNotification);
4646
}
47+
public event EventHandler ForceExpired;
48+
4749
private void ForceExpireNotification(object obj)
4850
{
51+
ExpirationTime = DateTime.Now;
4952
Expired = true;
53+
ForceExpired?.Invoke(this, EventArgs.Empty);
5054
}
5155
}
5256
}

0 commit comments

Comments
 (0)