Popup auffaellig, bleibt waehrend Klingeln offen, Tray-Icon blinkt bis Anrufliste geoeffnet
This commit is contained in:
parent
4376619459
commit
873c437d98
6 changed files with 115 additions and 40 deletions
|
|
@ -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();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,46 @@
|
||||||
<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">
|
||||||
<Grid.RowDefinitions>
|
<Border.Effect>
|
||||||
<RowDefinition Height="Auto" />
|
<DropShadowEffect BlurRadius="14" ShadowDepth="0" Opacity="0.55" Color="Black" />
|
||||||
<RowDefinition Height="Auto" />
|
</Border.Effect>
|
||||||
<RowDefinition Height="Auto" />
|
<Grid Margin="16,12,16,12">
|
||||||
<RowDefinition Height="Auto" />
|
<Grid.RowDefinitions>
|
||||||
</Grid.RowDefinitions>
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<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" />
|
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>
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue