Popup auffaellig, bleibt waehrend Klingeln offen, Tray-Icon blinkt bis Anrufliste geoeffnet

This commit is contained in:
dimedtec 2026-10-07 16:51:46 +02:00
parent 4376619459
commit 873c437d98
6 changed files with 115 additions and 40 deletions

View file

@ -79,13 +79,20 @@ public partial class App : System.Windows.Application
var logger = _host.Services.GetRequiredService<ILogger<App>>(); var logger = _host.Services.GetRequiredService<ILogger<App>>();
var cache = _host.Services.GetRequiredService<ContactCache>(); var cache = _host.Services.GetRequiredService<ContactCache>();
CallPopupWindow.Corner = _host.Services.GetRequiredService<IConfiguration>()["Popup:Corner"] ?? "BottomRight";
var endedCallIds = new HashSet<string>();
_trayIconManager = new TrayIconManager( _trayIconManager = new TrayIconManager(
onSyncNow: async () => onSyncNow: async () =>
{ {
var syncService = _host.Services.GetRequiredService<ContactSyncService>(); var syncService = _host.Services.GetRequiredService<ContactSyncService>();
await syncService.SyncOnceAsync(CancellationToken.None); await syncService.SyncOnceAsync(CancellationToken.None);
}, },
onShowHistory: () => CallHistoryWindow.ShowAsync(cache), onShowHistory: () =>
{
_trayIconManager?.StopAttention();
return CallHistoryWindow.ShowAsync(cache);
},
onExit: () => System.Windows.Application.Current.Shutdown()); onExit: () => System.Windows.Application.Current.Shutdown());
_trayIconManager.Show(); _trayIconManager.Show();
@ -112,6 +119,12 @@ public partial class App : System.Windows.Application
System.Windows.Application.Current.Dispatcher.Invoke(() => System.Windows.Application.Current.Dispatcher.Invoke(() =>
{ {
// Anruf war schon beendet, bevor der Kontakt aufgeloest war: kein Popup mehr noetig
if (endedCallIds.Remove(args.CallId))
{
return;
}
var popup = new CallPopupWindow(contact, args.CallId, args.CalledNumber); var popup = new CallPopupWindow(contact, args.CallId, args.CalledNumber);
popup.Show(); popup.Show();
}); });
@ -127,6 +140,10 @@ public partial class App : System.Windows.Application
System.Windows.Application.Current.Dispatcher.Invoke(() => System.Windows.Application.Current.Dispatcher.Invoke(() =>
{ {
CallPopupWindow.CloseByCallId(args.CallId); CallPopupWindow.CloseByCallId(args.CallId);
endedCallIds.Add(args.CallId);
// Klingeln beendet: Tray-Icon blinkt, bis die Anrufliste geoeffnet wird.
_trayIconManager?.StartAttention();
}); });
}; };

View file

@ -1,17 +1,22 @@
<Window x:Class="Anrufmonitor.App.CallPopupWindow" <Window x:Class="Anrufmonitor.App.CallPopupWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Anrufmonitor" Title="Anrufmonitor"
Width="360" Height="160" Width="380" Height="190"
WindowStyle="None" WindowStyle="None"
AllowsTransparency="True" AllowsTransparency="True"
Background="Transparent" Background="Transparent"
ShowInTaskbar="False" ShowInTaskbar="False"
Topmost="True" Topmost="True"
ShowActivated="False"
ResizeMode="NoResize" ResizeMode="NoResize"
SizeToContent="Manual"> SizeToContent="Manual">
<Border Background="#FF1F2430" CornerRadius="10" BorderBrush="#FF3A4152" BorderThickness="1"> <Grid Margin="10">
<Grid Margin="16"> <Border Background="White" CornerRadius="10" BorderBrush="#FFFF8C00" BorderThickness="4">
<Border.Effect>
<DropShadowEffect BlurRadius="14" ShadowDepth="0" Opacity="0.55" Color="Black" />
</Border.Effect>
<Grid Margin="16,12,16,12">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@ -19,22 +24,23 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Eingehender Anruf" Foreground="#FF8CA0C4" FontSize="12" FontWeight="SemiBold" /> <TextBlock Grid.Row="0" Text="📞 Eingehender Anruf" Foreground="#FFE67300" FontSize="13" FontWeight="Bold" />
<TextBlock Grid.Row="1" x:Name="NameText" Text="Unbekannt" Foreground="White" FontSize="22" FontWeight="Bold" <TextBlock Grid.Row="1" x:Name="NameText" Text="Unbekannt" Foreground="#FF111111" FontSize="24" FontWeight="Bold"
Margin="0,4,0,0" TextTrimming="CharacterEllipsis" /> Margin="0,4,0,0" TextTrimming="CharacterEllipsis" />
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,4,0,0"> <StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,4,0,0">
<TextBlock x:Name="CompanyText" Foreground="#FFB6C0D6" FontSize="14" Margin="0,0,8,0" /> <TextBlock x:Name="CompanyText" Foreground="#FF333333" FontSize="14" Margin="0,0,8,0" />
<TextBlock x:Name="NumberText" Foreground="#FF7C879C" FontSize="14" /> <TextBlock x:Name="NumberText" Foreground="#FF555555" FontSize="14" />
</StackPanel> </StackPanel>
<TextBlock Grid.Row="3" x:Name="CalledText" Foreground="#FF8CA0C4" FontSize="12" Margin="0,6,0,0" /> <TextBlock Grid.Row="3" x:Name="CalledText" Foreground="#FFE67300" FontSize="13" FontWeight="SemiBold" Margin="0,6,0,0" />
<Button Content="✕" HorizontalAlignment="Right" VerticalAlignment="Top" <Button Content="✕" HorizontalAlignment="Right" VerticalAlignment="Top"
Width="24" Height="24" Padding="0" Margin="0,-16,-16,0" Width="24" Height="24" Padding="0" Margin="0,-6,-6,0"
Background="Transparent" BorderThickness="0" Foreground="#FF7C879C" Background="Transparent" BorderThickness="0" Foreground="#FF555555"
Click="OnCloseClicked" /> Click="OnCloseClicked" />
</Grid> </Grid>
</Border> </Border>
</Grid>
</Window> </Window>

View file

@ -5,16 +5,19 @@ using Anrufmonitor.Contacts;
namespace Anrufmonitor.App; namespace Anrufmonitor.App;
/// <summary> /// <summary>
/// Toast-artiges Popup unten rechts, zeigt den (per <see cref="ContactLookupService"/> /// Popup, zeigt den (per <see cref="ContactLookupService"/> aufgeloesten) Anrufer an.
/// aufgeloesten) Anrufer an. Schliesst sich automatisch nach <see cref="AutoCloseAfter"/> /// Bleibt offen, solange es klingelt: geschlossen wird per <see cref="CloseByCallId"/>
/// oder wenn der Anruf beendet wird (<see cref="CloseByCallId"/>, ausgeloest durch /// (SIP CANCEL/Ring-Timeout) oder per X. <see cref="AutoCloseAfter"/> ist nur eine
/// SIP CANCEL/Ring-Timeout). /// Sicherung, falls das Ende des Anrufs nie signalisiert wird.
/// </summary> /// </summary>
public partial class CallPopupWindow : Window public partial class CallPopupWindow : Window
{ {
private static readonly TimeSpan AutoCloseAfter = TimeSpan.FromSeconds(20); private static readonly TimeSpan AutoCloseAfter = TimeSpan.FromMinutes(2);
private static readonly Dictionary<string, CallPopupWindow> OpenPopups = new(); private static readonly Dictionary<string, CallPopupWindow> OpenPopups = new();
/// <summary>Bildschirmecke fuer das Popup (Konfiguration "Popup:Corner"): BottomRight, BottomLeft, TopRight, TopLeft.</summary>
public static string Corner { get; set; } = "BottomRight";
private readonly string _callId; private readonly string _callId;
private readonly DispatcherTimer _autoCloseTimer; private readonly DispatcherTimer _autoCloseTimer;
@ -33,7 +36,7 @@ public partial class CallPopupWindow : Window
CompanyText.Visibility = string.IsNullOrWhiteSpace(contact.Company) ? Visibility.Collapsed : Visibility.Visible; CompanyText.Visibility = string.IsNullOrWhiteSpace(contact.Company) ? Visibility.Collapsed : Visibility.Visible;
NumberText.Text = contact.PhoneNumbers.Count > 0 ? contact.PhoneNumbers[0] : string.Empty; NumberText.Text = contact.PhoneNumbers.Count > 0 ? contact.PhoneNumbers[0] : string.Empty;
PositionBottomRight(); PositionOnScreen();
_autoCloseTimer = new DispatcherTimer { Interval = AutoCloseAfter }; _autoCloseTimer = new DispatcherTimer { Interval = AutoCloseAfter };
_autoCloseTimer.Tick += (_, _) => Close(); _autoCloseTimer.Tick += (_, _) => Close();
@ -57,11 +60,18 @@ public partial class CallPopupWindow : Window
} }
} }
private void PositionBottomRight() private void PositionOnScreen()
{ {
var workArea = SystemParameters.WorkArea; // Mehrere gleichzeitige Popups werden gestapelt statt uebereinander gelegt.
Left = workArea.Right - Width - 16; var stackIndex = Math.Max(0, OpenPopups.Count - 1);
Top = workArea.Bottom - Height - 16; var offset = stackIndex * (Height - 20);
var area = SystemParameters.WorkArea;
var right = Corner.EndsWith("Right", StringComparison.OrdinalIgnoreCase);
var bottom = Corner.StartsWith("Bottom", StringComparison.OrdinalIgnoreCase);
Left = right ? area.Right - Width : area.Left;
Top = bottom ? area.Bottom - Height - offset : area.Top + offset;
} }
private void OnCloseClicked(object sender, RoutedEventArgs e) => Close(); private void OnCloseClicked(object sender, RoutedEventArgs e) => Close();

View file

@ -14,11 +14,13 @@ public static class TrayIconFactory
public static readonly Color ConnectedColor = Color.FromArgb(255, 76, 175, 80); public static readonly Color ConnectedColor = Color.FromArgb(255, 76, 175, 80);
public static readonly Color DisconnectedColor = Color.FromArgb(255, 229, 57, 53); public static readonly Color DisconnectedColor = Color.FromArgb(255, 229, 57, 53);
public static readonly Color UnknownColor = Color.FromArgb(255, 158, 158, 158); public static readonly Color UnknownColor = Color.FromArgb(255, 158, 158, 158);
public static readonly Color AttentionColor = Color.FromArgb(255, 255, 140, 0);
[DllImport("user32.dll", SetLastError = true)] [DllImport("user32.dll", SetLastError = true)]
private static extern bool DestroyIcon(IntPtr handle); private static extern bool DestroyIcon(IntPtr handle);
public static Icon CreateStatusIcon(Color dotColor) /// <param name="alert">true = kompletter Kreis in Warnfarbe (zum Blinken bei neuem Anruf).</param>
public static Icon CreateStatusIcon(Color dotColor, bool alert = false)
{ {
using var bitmap = new Bitmap(32, 32); using var bitmap = new Bitmap(32, 32);
using (var g = Graphics.FromImage(bitmap)) using (var g = Graphics.FromImage(bitmap))
@ -26,7 +28,7 @@ public static class TrayIconFactory
g.SmoothingMode = SmoothingMode.AntiAlias; g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(Color.Transparent); g.Clear(Color.Transparent);
using var baseBrush = new SolidBrush(Color.FromArgb(255, 45, 55, 72)); using var baseBrush = new SolidBrush(alert ? AttentionColor : Color.FromArgb(255, 45, 55, 72));
g.FillEllipse(baseBrush, 1, 1, 28, 28); g.FillEllipse(baseBrush, 1, 1, 28, 28);
using var phonePen = new Pen(Color.White, 2.5f) { StartCap = LineCap.Round, EndCap = LineCap.Round }; using var phonePen = new Pen(Color.White, 2.5f) { StartCap = LineCap.Round, EndCap = LineCap.Round };

View file

@ -11,7 +11,11 @@ namespace Anrufmonitor.App;
public sealed class TrayIconManager : IDisposable public sealed class TrayIconManager : IDisposable
{ {
private readonly NotifyIcon _notifyIcon; private readonly NotifyIcon _notifyIcon;
private readonly System.Windows.Forms.Timer _flashTimer = new() { Interval = 600 };
private Icon? _currentIcon; private Icon? _currentIcon;
private Color _statusColor = TrayIconFactory.UnknownColor;
private bool _flashing;
private bool _flashOn;
public TrayIconManager(Func<Task> onSyncNow, Func<Task> onShowHistory, System.Action onExit) public TrayIconManager(Func<Task> onSyncNow, Func<Task> onShowHistory, System.Action onExit)
{ {
@ -41,19 +45,50 @@ public sealed class TrayIconManager : IDisposable
Visible = false, Visible = false,
}; };
_notifyIcon.DoubleClick += async (_, _) => await onShowHistory(); _notifyIcon.DoubleClick += async (_, _) => await onShowHistory();
_flashTimer.Tick += (_, _) =>
{
_flashOn = !_flashOn;
SetIcon(_statusColor, alert: _flashOn);
};
}
/// <summary>Laesst das Tray-Icon blinken (neuer Anruf), bis <see cref="StopAttention"/> aufgerufen wird.</summary>
public void StartAttention()
{
if (_flashing)
{
return;
}
_flashing = true;
_flashTimer.Start();
}
public void StopAttention()
{
if (!_flashing)
{
return;
}
_flashing = false;
_flashOn = false;
_flashTimer.Stop();
SetIcon(_statusColor, alert: false);
} }
public void Show() => _notifyIcon.Visible = true; public void Show() => _notifyIcon.Visible = true;
public void SetSipStatus(bool registered) public void SetSipStatus(bool registered)
{ {
SetIcon(registered ? TrayIconFactory.ConnectedColor : TrayIconFactory.DisconnectedColor); _statusColor = registered ? TrayIconFactory.ConnectedColor : TrayIconFactory.DisconnectedColor;
SetIcon(_statusColor, alert: _flashing && _flashOn);
_notifyIcon.Text = registered ? "Anrufmonitor - verbunden" : "Anrufmonitor - SIP getrennt"; _notifyIcon.Text = registered ? "Anrufmonitor - verbunden" : "Anrufmonitor - SIP getrennt";
} }
private void SetIcon(Color dotColor) private void SetIcon(Color dotColor, bool alert = false)
{ {
var newIcon = TrayIconFactory.CreateStatusIcon(dotColor); var newIcon = TrayIconFactory.CreateStatusIcon(dotColor, alert);
var oldIcon = _currentIcon; var oldIcon = _currentIcon;
_notifyIcon.Icon = newIcon; _notifyIcon.Icon = newIcon;
@ -72,6 +107,8 @@ public sealed class TrayIconManager : IDisposable
public void Dispose() public void Dispose()
{ {
_flashTimer.Stop();
_flashTimer.Dispose();
_notifyIcon.Visible = false; _notifyIcon.Visible = false;
_notifyIcon.Dispose(); _notifyIcon.Dispose();
_currentIcon?.Dispose(); _currentIcon?.Dispose();

View file

@ -29,6 +29,9 @@
"_comment": "Reverse-Lookup gegen dasoertliche.de - keine offizielle API, siehe PublicDirectoryLookupProvider.cs" "_comment": "Reverse-Lookup gegen dasoertliche.de - keine offizielle API, siehe PublicDirectoryLookupProvider.cs"
} }
}, },
"Popup": {
"Corner": "BottomRight"
},
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",