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 cache = _host.Services.GetRequiredService<ContactCache>();
CallPopupWindow.Corner = _host.Services.GetRequiredService<IConfiguration>()["Popup:Corner"] ?? "BottomRight";
var endedCallIds = new HashSet<string>();
_trayIconManager = new TrayIconManager(
onSyncNow: async () =>
{
var syncService = _host.Services.GetRequiredService<ContactSyncService>();
await syncService.SyncOnceAsync(CancellationToken.None);
},
onShowHistory: () => CallHistoryWindow.ShowAsync(cache),
onShowHistory: () =>
{
_trayIconManager?.StopAttention();
return CallHistoryWindow.ShowAsync(cache);
},
onExit: () => System.Windows.Application.Current.Shutdown());
_trayIconManager.Show();
@ -112,6 +119,12 @@ public partial class App : System.Windows.Application
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);
popup.Show();
});
@ -127,6 +140,10 @@ public partial class App : System.Windows.Application
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
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:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Anrufmonitor"
Width="360" Height="160"
Width="380" Height="190"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
ShowInTaskbar="False"
Topmost="True"
ShowActivated="False"
ResizeMode="NoResize"
SizeToContent="Manual">
<Border Background="#FF1F2430" CornerRadius="10" BorderBrush="#FF3A4152" BorderThickness="1">
<Grid Margin="16">
<Grid Margin="10">
<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>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
@ -19,22 +24,23 @@
<RowDefinition Height="Auto" />
</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" />
<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="NumberText" Foreground="#FF7C879C" FontSize="14" />
<TextBlock x:Name="CompanyText" Foreground="#FF333333" FontSize="14" Margin="0,0,8,0" />
<TextBlock x:Name="NumberText" Foreground="#FF555555" FontSize="14" />
</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"
Width="24" Height="24" Padding="0" Margin="0,-16,-16,0"
Background="Transparent" BorderThickness="0" Foreground="#FF7C879C"
Width="24" Height="24" Padding="0" Margin="0,-6,-6,0"
Background="Transparent" BorderThickness="0" Foreground="#FF555555"
Click="OnCloseClicked" />
</Grid>
</Border>
</Grid>
</Window>

View file

@ -5,16 +5,19 @@ using Anrufmonitor.Contacts;
namespace Anrufmonitor.App;
/// <summary>
/// Toast-artiges Popup unten rechts, zeigt den (per <see cref="ContactLookupService"/>
/// aufgeloesten) Anrufer an. Schliesst sich automatisch nach <see cref="AutoCloseAfter"/>
/// oder wenn der Anruf beendet wird (<see cref="CloseByCallId"/>, ausgeloest durch
/// SIP CANCEL/Ring-Timeout).
/// Popup, zeigt den (per <see cref="ContactLookupService"/> aufgeloesten) Anrufer an.
/// Bleibt offen, solange es klingelt: geschlossen wird per <see cref="CloseByCallId"/>
/// (SIP CANCEL/Ring-Timeout) oder per X. <see cref="AutoCloseAfter"/> ist nur eine
/// Sicherung, falls das Ende des Anrufs nie signalisiert wird.
/// </summary>
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();
/// <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 DispatcherTimer _autoCloseTimer;
@ -33,7 +36,7 @@ public partial class CallPopupWindow : Window
CompanyText.Visibility = string.IsNullOrWhiteSpace(contact.Company) ? Visibility.Collapsed : Visibility.Visible;
NumberText.Text = contact.PhoneNumbers.Count > 0 ? contact.PhoneNumbers[0] : string.Empty;
PositionBottomRight();
PositionOnScreen();
_autoCloseTimer = new DispatcherTimer { Interval = AutoCloseAfter };
_autoCloseTimer.Tick += (_, _) => Close();
@ -57,11 +60,18 @@ public partial class CallPopupWindow : Window
}
}
private void PositionBottomRight()
private void PositionOnScreen()
{
var workArea = SystemParameters.WorkArea;
Left = workArea.Right - Width - 16;
Top = workArea.Bottom - Height - 16;
// Mehrere gleichzeitige Popups werden gestapelt statt uebereinander gelegt.
var stackIndex = Math.Max(0, OpenPopups.Count - 1);
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();

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 DisconnectedColor = Color.FromArgb(255, 229, 57, 53);
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)]
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 g = Graphics.FromImage(bitmap))
@ -26,7 +28,7 @@ public static class TrayIconFactory
g.SmoothingMode = SmoothingMode.AntiAlias;
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);
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
{
private readonly NotifyIcon _notifyIcon;
private readonly System.Windows.Forms.Timer _flashTimer = new() { Interval = 600 };
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)
{
@ -41,19 +45,50 @@ public sealed class TrayIconManager : IDisposable
Visible = false,
};
_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 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";
}
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;
_notifyIcon.Icon = newIcon;
@ -72,6 +107,8 @@ public sealed class TrayIconManager : IDisposable
public void Dispose()
{
_flashTimer.Stop();
_flashTimer.Dispose();
_notifyIcon.Visible = false;
_notifyIcon.Dispose();
_currentIcon?.Dispose();

View file

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