Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
NuGetAudit + TreatWarningsAsErrors the advisory also fails restore, so
staying on 2025.1.0 is not an option anyway. -->
<PackageVersion Include="SSH.NET" Version="2026.0.0" />
<!-- E-mail action for camera events (SMTP transport). MailKit rather than
System.Net.Mail: the latter cannot do implicit TLS on port 465, which
Mail.ru / Yandex and many corporate relays expect. -->
<PackageVersion Include="MailKit" Version="4.18.1" />
<!-- Avalonia.Skia 12.0.3 pulls SkiaSharp 3.119.4-preview.1.1 transitively; match it explicitly. -->
<PackageVersion Include="SkiaSharp" Version="3.119.4-preview.1.1" />
<!-- Linux native libSkiaSharp for the Avalonia-less test process. The
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,14 @@ Built with .NET 9 / 10 and Avalonia 12.
snapshots over SSH.
- **Health & status** — unified per-camera verdict (online / attention /
offline) across grid, library and a Health Center overview.
- **Notifications** — system notifications for motion and detections, with
quiet hours.
- **Notifications** — alerts for motion and detections with thresholds (AI
confidence, event duration, objects in frame), cooldown and quiet hours.
Desktop: the app's own toast with a built-in or custom sound and volume, or
native Windows notifications managed in Windows Settings.
- **External actions** — react to an event outside the app: a TCP/UDP signal
(text or raw hex) to an alarm panel or relay, an HTTP webhook, an e-mail
(SMTP with the login kept in Windows Credential Manager, the default mail
client via MAPI, or Outlook), or a local program/script.
- **Desktop niceties** — tray icon with quick actions, optional
close-to-tray, choice of start page (Library or Live) and startup layout,
light/dark themes with accent colors.
Expand Down
40 changes: 36 additions & 4 deletions src/OpenIPC.Viewer.App/Services/DesktopNotificationService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
Expand All @@ -7,27 +8,58 @@

namespace OpenIPC.Viewer.App.Services;

// Desktop notification sink (Phase 19.3): stacks Telegram-style toast windows in
// the bottom-right of the primary screen. The coordinator already vetted the
// notification (cooldown / quiet hours / type) — we just present it. Marshals to
// the UI thread since events arrive off the decode/event pipeline.
// Desktop notification sink (Phase 19.3). The coordinator already vetted the
// notification (thresholds / cooldown / quiet hours / type); this decides how it
// looks and sounds, per the "Notification style" setting:
// "app" — Telegram-style toast windows stacked bottom-right of the primary
// screen, plus the app's own sound at the chosen volume;
// "windows" — a native Windows toast (ISystemNotificationSink); banners, sound
// and Do Not Disturb are then governed by Windows Settings.
// Marshals to the UI thread since events arrive off the decode/event pipeline.
public sealed class DesktopNotificationService : INotificationService
{
public const string StyleApp = "app";
public const string StyleWindows = "windows";

// Newest at the bottom; older ones float up. Cap so a burst doesn't paper
// over the whole screen — the oldest is dropped when the cap is hit.
private const int MaxVisible = 4;
private const double MarginDip = 16;
private const double GapDip = 10;

private readonly UserSettingsService _settings;
private readonly NotificationSoundService _sound;
private readonly ISystemNotificationSink? _system;
private readonly List<ToastWindow> _active = new();

public DesktopNotificationService(
UserSettingsService settings,
NotificationSoundService sound,
ISystemNotificationSink? system = null)
{
_settings = settings;
_sound = sound;
_system = system;
}

// Always available on desktop — this sink is only registered in the desktop
// head. (We can't probe Application.Current here: the coordinator is Start()ed
// during composition, before the Avalonia lifetime exists.)
public bool IsAvailable => true;

public bool UsesSystemNotifications =>
_system is { IsSupported: true } &&
string.Equals(_settings.Current.NotificationStyle, StyleWindows, StringComparison.OrdinalIgnoreCase);

public void Show(NotificationRequest request)
{
if (UsesSystemNotifications)
{
_system!.Show(request);
return;
}

_sound.PlayForNotification();
if (Dispatcher.UIThread.CheckAccess()) ShowCore(request);
else Dispatcher.UIThread.Post(() => ShowCore(request));
}
Expand Down
31 changes: 31 additions & 0 deletions src/OpenIPC.Viewer.App/Services/ISystemNotificationSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using OpenIPC.Viewer.Core.Notifications;

namespace OpenIPC.Viewer.App.Services;

// The operating system's own notification center as an alternative to the
// app's corner toast (desktop "Notification style" setting). Today that is the
// Windows Action Center: the app shows up in Settings → System → Notifications,
// where the user controls banners, sound, priority and Do Not Disturb with the
// standard Windows tools. Registered by the Windows head only.
public interface ISystemNotificationSink
{
// False on systems without native toasts (e.g. before Windows 10).
bool IsSupported { get; }

// Whether Windows will actually display our toasts right now.
SystemNotificationState State { get; }

void Show(NotificationRequest request);

// Opens the OS notification settings page.
void OpenSettings();
}

public enum SystemNotificationState
{
Enabled,
DisabledForApp,
DisabledForUser,
DisabledByPolicy,
Unavailable,
}
Loading