mirror of
https://github.com/LANCommander/Notify.NET.git
synced 2026-08-01 03:08:24 -04:00
Add support for inline images, attribution text, image crop hint, audio files, custom icon path in Windows
This commit is contained in:
parent
e1166c474e
commit
a24e3cd46d
7 changed files with 411 additions and 45 deletions
|
|
@ -20,6 +20,9 @@
|
|||
#define WINTOASTWRAPPER_EXPORTS
|
||||
#define NOMINMAX
|
||||
#include <Windows.h>
|
||||
#include <shlobj.h> // SHGetFolderPathW, IShellLinkW, CLSID_ShellLink
|
||||
#include <objbase.h> // CoCreateInstance
|
||||
#include <strsafe.h> // StringCchCatW
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
|
@ -195,7 +198,7 @@ NOTIFYAPI BOOL WNT_IsCompatible(void)
|
|||
return WinToast::isCompatible() ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
NOTIFYAPI BOOL WNT_Initialize(const wchar_t* appName, const wchar_t* appUserModelId)
|
||||
NOTIFYAPI BOOL WNT_Initialize(const wchar_t* appName, const wchar_t* appUserModelId, const wchar_t* appIconPath)
|
||||
{
|
||||
if (!WinToast::isCompatible())
|
||||
return FALSE;
|
||||
|
|
@ -213,6 +216,41 @@ NOTIFYAPI BOOL WNT_Initialize(const wchar_t* appName, const wchar_t* appUserMode
|
|||
if (!instance->initialize(&error))
|
||||
return FALSE;
|
||||
|
||||
// If a custom app icon was requested, stamp it onto the Start-Menu shortcut
|
||||
// that WinToastLib just created/verified. This icon appears in the top-left
|
||||
// corner of every toast notification from this app.
|
||||
if (appIconPath && appIconPath[0] != L'\0')
|
||||
{
|
||||
// Build the same shortcut path WinToastLib uses: %APPDATA%\Microsoft\Windows\Start Menu\Programs\{appName}.lnk
|
||||
WCHAR linkPath[MAX_PATH] = {};
|
||||
if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, linkPath)))
|
||||
{
|
||||
if (SUCCEEDED(StringCchCatW(linkPath, MAX_PATH, L"\\Microsoft\\Windows\\Start Menu\\Programs\\")) &&
|
||||
SUCCEEDED(StringCchCatW(linkPath, MAX_PATH, appName)) &&
|
||||
SUCCEEDED(StringCchCatW(linkPath, MAX_PATH, L".lnk")))
|
||||
{
|
||||
IShellLinkW* shellLink = nullptr;
|
||||
if (SUCCEEDED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
|
||||
IID_IShellLinkW, reinterpret_cast<void**>(&shellLink))))
|
||||
{
|
||||
IPersistFile* persistFile = nullptr;
|
||||
if (SUCCEEDED(shellLink->QueryInterface(IID_IPersistFile,
|
||||
reinterpret_cast<void**>(&persistFile))))
|
||||
{
|
||||
if (SUCCEEDED(persistFile->Load(linkPath, STGM_READWRITE)))
|
||||
{
|
||||
shellLink->SetIconLocation(appIconPath, 0);
|
||||
persistFile->Save(linkPath, TRUE);
|
||||
}
|
||||
persistFile->Release();
|
||||
}
|
||||
shellLink->Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Icon update is best-effort — do not fail initialization if it doesn't succeed.
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
// Clear lookup table from a previous Initialize/Uninitialize cycle.
|
||||
|
|
@ -237,9 +275,10 @@ NOTIFYAPI INT64 WNT_ShowToast(
|
|||
if (!descriptor || !handler)
|
||||
return static_cast<INT64>(WinToast::WinToastError::InvalidParameters);
|
||||
|
||||
bool hasBody = descriptor->body != nullptr && descriptor->body[0] != L'\0';
|
||||
bool hasImage = descriptor->imagePath != nullptr && descriptor->imagePath[0] != L'\0';
|
||||
bool hasHeroImage = descriptor->heroImagePath != nullptr && descriptor->heroImagePath[0] != L'\0';
|
||||
bool hasBody = descriptor->body != nullptr && descriptor->body[0] != L'\0';
|
||||
bool hasImage = descriptor->imagePath != nullptr && descriptor->imagePath[0] != L'\0';
|
||||
bool hasHeroImage = descriptor->heroImagePath != nullptr && descriptor->heroImagePath[0] != L'\0';
|
||||
bool hasInlineImage = descriptor->inlineImagePath != nullptr && descriptor->inlineImagePath[0] != L'\0';
|
||||
|
||||
WinToastTemplate tmpl(SelectTemplateType(hasImage, hasBody));
|
||||
|
||||
|
|
@ -248,10 +287,21 @@ NOTIFYAPI INT64 WNT_ShowToast(
|
|||
tmpl.setTextField(descriptor->body, WinToastTemplate::SecondLine);
|
||||
|
||||
if (hasImage)
|
||||
tmpl.setImagePath(descriptor->imagePath);
|
||||
{
|
||||
WinToastTemplate::CropHint cropHint = (descriptor->cropHint == WNT_CROP_HINT_CIRCLE)
|
||||
? WinToastTemplate::CropHint::Circle
|
||||
: WinToastTemplate::CropHint::Square;
|
||||
tmpl.setImagePath(descriptor->imagePath, cropHint);
|
||||
}
|
||||
|
||||
if (hasHeroImage)
|
||||
tmpl.setHeroImagePath(descriptor->heroImagePath);
|
||||
// Inline image takes precedence over hero image; only one can be set at a time.
|
||||
if (hasInlineImage)
|
||||
tmpl.setHeroImagePath(descriptor->inlineImagePath, true /* inline */);
|
||||
else if (hasHeroImage)
|
||||
tmpl.setHeroImagePath(descriptor->heroImagePath, false /* banner */);
|
||||
|
||||
if (descriptor->attributionText != nullptr && descriptor->attributionText[0] != L'\0')
|
||||
tmpl.setAttributionText(descriptor->attributionText);
|
||||
|
||||
for (int i = 0; i < descriptor->buttonCount; ++i)
|
||||
{
|
||||
|
|
@ -262,6 +312,16 @@ NOTIFYAPI INT64 WNT_ShowToast(
|
|||
if (descriptor->expirationMs > 0)
|
||||
tmpl.setExpiration(descriptor->expirationMs);
|
||||
|
||||
// Audio: custom path overrides the system-sound enum; both are independent of AudioOption.
|
||||
if (descriptor->customAudioPath != nullptr && descriptor->customAudioPath[0] != L'\0')
|
||||
{
|
||||
tmpl.setAudioPath(descriptor->customAudioPath);
|
||||
}
|
||||
else if (descriptor->audioFile >= 0)
|
||||
{
|
||||
tmpl.setAudioPath(static_cast<WinToastTemplate::AudioSystemFile>(descriptor->audioFile));
|
||||
}
|
||||
|
||||
switch (descriptor->audioOption)
|
||||
{
|
||||
case WNT_AUDIO_SILENT:
|
||||
|
|
|
|||
|
|
@ -62,21 +62,79 @@ typedef enum _WNT_AudioOption {
|
|||
WNT_AUDIO_LOOP = 2
|
||||
} WNT_AudioOption;
|
||||
|
||||
/**
|
||||
* Selects which Windows system notification sound to play.
|
||||
* WNT_AUDIO_FILE_NONE (-1) means no specific sound file override (use AudioOption behaviour).
|
||||
* Values 0-25 map directly to WinToastTemplate::AudioSystemFile.
|
||||
*/
|
||||
typedef enum _WNT_AudioFile {
|
||||
WNT_AUDIO_FILE_NONE = -1,
|
||||
WNT_AUDIO_FILE_DEFAULT = 0,
|
||||
WNT_AUDIO_FILE_IM = 1,
|
||||
WNT_AUDIO_FILE_MAIL = 2,
|
||||
WNT_AUDIO_FILE_REMINDER = 3,
|
||||
WNT_AUDIO_FILE_SMS = 4,
|
||||
WNT_AUDIO_FILE_ALARM = 5,
|
||||
WNT_AUDIO_FILE_ALARM2 = 6,
|
||||
WNT_AUDIO_FILE_ALARM3 = 7,
|
||||
WNT_AUDIO_FILE_ALARM4 = 8,
|
||||
WNT_AUDIO_FILE_ALARM5 = 9,
|
||||
WNT_AUDIO_FILE_ALARM6 = 10,
|
||||
WNT_AUDIO_FILE_ALARM7 = 11,
|
||||
WNT_AUDIO_FILE_ALARM8 = 12,
|
||||
WNT_AUDIO_FILE_ALARM9 = 13,
|
||||
WNT_AUDIO_FILE_ALARM10 = 14,
|
||||
WNT_AUDIO_FILE_CALL = 15,
|
||||
WNT_AUDIO_FILE_CALL1 = 16,
|
||||
WNT_AUDIO_FILE_CALL2 = 17,
|
||||
WNT_AUDIO_FILE_CALL3 = 18,
|
||||
WNT_AUDIO_FILE_CALL4 = 19,
|
||||
WNT_AUDIO_FILE_CALL5 = 20,
|
||||
WNT_AUDIO_FILE_CALL6 = 21,
|
||||
WNT_AUDIO_FILE_CALL7 = 22,
|
||||
WNT_AUDIO_FILE_CALL8 = 23,
|
||||
WNT_AUDIO_FILE_CALL9 = 24,
|
||||
WNT_AUDIO_FILE_CALL10 = 25
|
||||
} WNT_AudioFile;
|
||||
|
||||
/** Controls how the app-logo image is cropped. */
|
||||
typedef enum _WNT_CropHint {
|
||||
WNT_CROP_HINT_SQUARE = 0,
|
||||
WNT_CROP_HINT_CIRCLE = 1
|
||||
} WNT_CropHint;
|
||||
|
||||
/**
|
||||
* Describes the notification to display.
|
||||
* All pointer fields may be NULL where noted.
|
||||
* Callers must keep pointed-to memory valid for the duration of WNT_ShowToast.
|
||||
*
|
||||
* Field layout (x64, no explicit packing):
|
||||
* offsets 0..39 — five pointers (title, body, imagePath, heroImagePath, buttonLabels)
|
||||
* offset 40 — buttonCount (int, 4 bytes) + 4 bytes natural padding
|
||||
* offset 48 — expirationMs (long long, 8 bytes)
|
||||
* offset 56 — scenario (int, 4 bytes)
|
||||
* offset 60 — audioOption (int, 4 bytes)
|
||||
* offsets 64..79 — three new pointers (inlineImagePath, attributionText, customAudioPath)
|
||||
* offset 88 — cropHint (int, 4 bytes)
|
||||
* offset 92 — audioFile (int, 4 bytes)
|
||||
* Total: 96 bytes
|
||||
*/
|
||||
typedef struct _WNT_ToastDescriptor {
|
||||
const wchar_t* title; /* required */
|
||||
const wchar_t* body; /* nullable */
|
||||
const wchar_t* imagePath; /* nullable — absolute path; displayed as a square thumbnail */
|
||||
const wchar_t* heroImagePath; /* nullable — absolute path; displayed full-width, aspect ratio preserved */
|
||||
const wchar_t** buttonLabels; /* nullable — array of buttonCount wchar_t* */
|
||||
const wchar_t* title; /* required */
|
||||
const wchar_t* body; /* nullable */
|
||||
const wchar_t* imagePath; /* nullable — absolute path; app logo override in generic templates */
|
||||
const wchar_t* heroImagePath; /* nullable — absolute path; full-width banner above the notification */
|
||||
const wchar_t** buttonLabels; /* nullable — array of buttonCount wchar_t* */
|
||||
int buttonCount;
|
||||
long long expirationMs; /* 0 = platform default */
|
||||
long long expirationMs; /* 0 = platform default */
|
||||
WNT_Scenario scenario;
|
||||
WNT_AudioOption audioOption;
|
||||
/* Extended fields (added in v2): */
|
||||
const wchar_t* inlineImagePath; /* nullable — image displayed inline inside the notification body */
|
||||
const wchar_t* attributionText; /* nullable — small text shown at the bottom of the notification */
|
||||
const wchar_t* customAudioPath; /* nullable — ms-winsoundevent: URI or ms-appx:/// path; overrides audioFile */
|
||||
int cropHint; /* WNT_CropHint — how imagePath is cropped (Square or Circle) */
|
||||
int audioFile; /* WNT_AudioFile — system sound to play; -1 = not overridden */
|
||||
} WNT_ToastDescriptor;
|
||||
|
||||
/**
|
||||
|
|
@ -102,9 +160,14 @@ typedef struct _WNT_Handler {
|
|||
* @param appUserModelId AppUserModelId (AUMI). The wrapper creates a Start-Menu
|
||||
* shortcut carrying this AUMI automatically if one does not
|
||||
* already exist.
|
||||
* @param appIconPath Optional absolute path to an .ico (or .exe/.dll) file whose
|
||||
* first icon is stamped onto the Start-Menu shortcut. This is
|
||||
* the small icon shown in the top-left corner of every toast
|
||||
* notification from this app. Pass NULL to use the default
|
||||
* (the host executable's icon).
|
||||
* @return TRUE on success.
|
||||
*/
|
||||
NOTIFYAPI BOOL WNT_Initialize(const wchar_t* appName, const wchar_t* appUserModelId);
|
||||
NOTIFYAPI BOOL WNT_Initialize(const wchar_t* appName, const wchar_t* appUserModelId, const wchar_t* appIconPath);
|
||||
|
||||
/**
|
||||
* Releases all WinToastLib resources. Call from the same STA thread as WNT_Initialize.
|
||||
|
|
|
|||
|
|
@ -16,15 +16,56 @@ namespace Notify.NET.Abstractions
|
|||
/// <summary>Optional body text shown beneath the title.</summary>
|
||||
public string? Body { get; }
|
||||
|
||||
/// <summary>Absolute path to an image file displayed as a square thumbnail.</summary>
|
||||
/// <summary>
|
||||
/// Absolute path to an image used as the app logo override — the small icon shown
|
||||
/// alongside the notification content. In generic toast templates (i.e. when a hero
|
||||
/// or inline image is also present, or when <see cref="ImageCropHint"/> is
|
||||
/// <see cref="NotificationImageCropHint.Circle"/>) this image replaces the default
|
||||
/// app icon. Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public string? ImagePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Absolute path to an image file displayed full-width above the title, preserving aspect ratio.
|
||||
/// Mutually exclusive with <see cref="InlineImagePath"/> — if both are set, the inline image takes precedence.
|
||||
/// Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public string? HeroImagePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Absolute path to an image file displayed inline inside the notification body.
|
||||
/// Takes precedence over <see cref="HeroImagePath"/> when both are set.
|
||||
/// Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public string? InlineImagePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Small attribution text shown at the bottom of the notification (e.g. a source name).
|
||||
/// Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public string? AttributionText { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls how <see cref="ImagePath"/> is cropped. Defaults to <see cref="NotificationImageCropHint.Square"/>.
|
||||
/// Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public NotificationImageCropHint ImageCropHint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A specific Windows system notification sound to play, independent of
|
||||
/// <see cref="Audio"/>. When set, overrides the default sound selection.
|
||||
/// Ignored if <see cref="CustomAudioPath"/> is also set.
|
||||
/// Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public NotificationAudioFile? AudioFile { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A custom audio URI (e.g. <c>ms-appx:///sounds/alert.mp3</c> or a
|
||||
/// <c>ms-winsoundevent:</c> URI). When set, takes precedence over <see cref="AudioFile"/>.
|
||||
/// Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public string? CustomAudioPath { get; }
|
||||
|
||||
/// <summary>Action buttons to display. Maximum platform limits apply (typically 5 on Windows, varies on Linux).</summary>
|
||||
public IReadOnlyList<NotificationButton> Buttons { get; }
|
||||
|
||||
|
|
@ -45,6 +86,11 @@ namespace Notify.NET.Abstractions
|
|||
string? body,
|
||||
string? imagePath,
|
||||
string? heroImagePath,
|
||||
string? inlineImagePath,
|
||||
string? attributionText,
|
||||
NotificationImageCropHint imageCropHint,
|
||||
NotificationAudioFile? audioFile,
|
||||
string? customAudioPath,
|
||||
IReadOnlyList<NotificationButton> buttons,
|
||||
INotificationHandler? handler,
|
||||
TimeSpan? expiration,
|
||||
|
|
@ -58,6 +104,11 @@ namespace Notify.NET.Abstractions
|
|||
Body = body;
|
||||
ImagePath = imagePath;
|
||||
HeroImagePath = heroImagePath;
|
||||
InlineImagePath = inlineImagePath;
|
||||
AttributionText = attributionText;
|
||||
ImageCropHint = imageCropHint;
|
||||
AudioFile = audioFile;
|
||||
CustomAudioPath = customAudioPath;
|
||||
Buttons = buttons;
|
||||
Handler = handler;
|
||||
Expiration = expiration;
|
||||
|
|
@ -66,6 +117,79 @@ namespace Notify.NET.Abstractions
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controls how <see cref="NotificationRequest.ImagePath"/> is cropped when displayed as the app logo override.
|
||||
/// Windows only.
|
||||
/// </summary>
|
||||
public enum NotificationImageCropHint
|
||||
{
|
||||
/// <summary>Display the image uncropped (square).</summary>
|
||||
Square = 0,
|
||||
/// <summary>Crop the image into a circle.</summary>
|
||||
Circle = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a Windows system notification sound.
|
||||
/// Set on <see cref="NotificationRequest.AudioFile"/> independently of
|
||||
/// <see cref="NotificationAudio"/> (which controls looping/silence behaviour).
|
||||
/// </summary>
|
||||
public enum NotificationAudioFile
|
||||
{
|
||||
/// <summary>The generic default notification sound.</summary>
|
||||
Default = 0,
|
||||
/// <summary>Instant message sound.</summary>
|
||||
IM = 1,
|
||||
/// <summary>New mail sound.</summary>
|
||||
Mail = 2,
|
||||
/// <summary>Reminder sound.</summary>
|
||||
Reminder = 3,
|
||||
/// <summary>SMS / text message sound.</summary>
|
||||
SMS = 4,
|
||||
/// <summary>Looping alarm sound (variant 1).</summary>
|
||||
Alarm = 5,
|
||||
/// <summary>Looping alarm sound (variant 2).</summary>
|
||||
Alarm2 = 6,
|
||||
/// <summary>Looping alarm sound (variant 3).</summary>
|
||||
Alarm3 = 7,
|
||||
/// <summary>Looping alarm sound (variant 4).</summary>
|
||||
Alarm4 = 8,
|
||||
/// <summary>Looping alarm sound (variant 5).</summary>
|
||||
Alarm5 = 9,
|
||||
/// <summary>Looping alarm sound (variant 6).</summary>
|
||||
Alarm6 = 10,
|
||||
/// <summary>Looping alarm sound (variant 7).</summary>
|
||||
Alarm7 = 11,
|
||||
/// <summary>Looping alarm sound (variant 8).</summary>
|
||||
Alarm8 = 12,
|
||||
/// <summary>Looping alarm sound (variant 9).</summary>
|
||||
Alarm9 = 13,
|
||||
/// <summary>Looping alarm sound (variant 10).</summary>
|
||||
Alarm10 = 14,
|
||||
/// <summary>Looping incoming-call sound (variant 1).</summary>
|
||||
Call = 15,
|
||||
/// <summary>Looping incoming-call sound (variant 2).</summary>
|
||||
Call1 = 16,
|
||||
/// <summary>Looping incoming-call sound (variant 3).</summary>
|
||||
Call2 = 17,
|
||||
/// <summary>Looping incoming-call sound (variant 4).</summary>
|
||||
Call3 = 18,
|
||||
/// <summary>Looping incoming-call sound (variant 5).</summary>
|
||||
Call4 = 19,
|
||||
/// <summary>Looping incoming-call sound (variant 6).</summary>
|
||||
Call5 = 20,
|
||||
/// <summary>Looping incoming-call sound (variant 7).</summary>
|
||||
Call6 = 21,
|
||||
/// <summary>Looping incoming-call sound (variant 8).</summary>
|
||||
Call7 = 22,
|
||||
/// <summary>Looping incoming-call sound (variant 9).</summary>
|
||||
Call8 = 23,
|
||||
/// <summary>Looping incoming-call sound (variant 10).</summary>
|
||||
Call9 = 24,
|
||||
/// <summary>Looping incoming-call sound (variant 11).</summary>
|
||||
Call10 = 25
|
||||
}
|
||||
|
||||
/// <summary>Controls the audio played when the notification is shown (Windows only; Linux ignores this).</summary>
|
||||
public enum NotificationAudio
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ namespace Notify.NET.Builder
|
|||
private string? _body;
|
||||
private string? _imagePath;
|
||||
private string? _heroImagePath;
|
||||
private string? _inlineImagePath;
|
||||
private string? _attributionText;
|
||||
private NotificationImageCropHint _imageCropHint = NotificationImageCropHint.Square;
|
||||
private NotificationAudioFile? _audioFile;
|
||||
private string? _customAudioPath;
|
||||
private readonly List<NotificationButton> _buttons = new List<NotificationButton>();
|
||||
private INotificationHandler? _handler;
|
||||
private TimeSpan? _expiration;
|
||||
|
|
@ -59,7 +64,12 @@ namespace Notify.NET.Builder
|
|||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Sets the absolute path of an image to display as a square thumbnail.</summary>
|
||||
/// <summary>
|
||||
/// Sets the absolute path of an image used as the app logo override — the small icon
|
||||
/// displayed alongside the notification content. In generic toast templates (when a hero
|
||||
/// or inline image is present, or when crop hint is <see cref="NotificationImageCropHint.Circle"/>)
|
||||
/// this replaces the default app icon. Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public NotificationBuilder WithImage(string imagePath)
|
||||
{
|
||||
_imagePath = imagePath;
|
||||
|
|
@ -68,7 +78,9 @@ namespace Notify.NET.Builder
|
|||
|
||||
/// <summary>
|
||||
/// Sets the absolute path of an image to display full-width above the notification title,
|
||||
/// preserving the image's aspect ratio. Windows only — ignored on Linux and macOS.
|
||||
/// preserving the image's aspect ratio.
|
||||
/// Mutually exclusive with <see cref="WithInlineImage"/>; if both are set the inline image takes precedence.
|
||||
/// Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public NotificationBuilder WithHeroImage(string imagePath)
|
||||
{
|
||||
|
|
@ -76,6 +88,62 @@ namespace Notify.NET.Builder
|
|||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the absolute path of an image displayed inline inside the notification body.
|
||||
/// Takes precedence over <see cref="WithHeroImage"/> when both are set.
|
||||
/// Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public NotificationBuilder WithInlineImage(string imagePath)
|
||||
{
|
||||
_inlineImagePath = imagePath;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets small attribution text shown at the bottom of the notification (e.g. a source name or URL).
|
||||
/// Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public NotificationBuilder WithAttributionText(string text)
|
||||
{
|
||||
_attributionText = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controls how <see cref="WithImage"/> is cropped when displayed as the app logo override.
|
||||
/// <see cref="NotificationImageCropHint.Circle"/> also forces the toast into generic template
|
||||
/// mode, which enables hero/inline images and attribution text.
|
||||
/// Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public NotificationBuilder WithImageCropHint(NotificationImageCropHint cropHint)
|
||||
{
|
||||
_imageCropHint = cropHint;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a specific Windows system notification sound. Overrides the default sound
|
||||
/// selection while still respecting the <see cref="WithAudio"/> loop/silence setting.
|
||||
/// Ignored when <see cref="WithCustomAudioPath"/> is also set.
|
||||
/// Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public NotificationBuilder WithAudioFile(NotificationAudioFile audioFile)
|
||||
{
|
||||
_audioFile = audioFile;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a custom audio URI (e.g. <c>ms-appx:///sounds/alert.mp3</c> or a
|
||||
/// <c>ms-winsoundevent:</c> URI). Takes precedence over <see cref="WithAudioFile"/>.
|
||||
/// Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public NotificationBuilder WithCustomAudioPath(string audioPath)
|
||||
{
|
||||
_customAudioPath = audioPath;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Adds an action button with an optional click callback.</summary>
|
||||
/// <param name="label">Text shown on the button.</param>
|
||||
/// <param name="callback">Called with the notification ID when the button is clicked.</param>
|
||||
|
|
@ -178,6 +246,11 @@ namespace Notify.NET.Builder
|
|||
body: _body,
|
||||
imagePath: _imagePath,
|
||||
heroImagePath: _heroImagePath,
|
||||
inlineImagePath: _inlineImagePath,
|
||||
attributionText: _attributionText,
|
||||
imageCropHint: _imageCropHint,
|
||||
audioFile: _audioFile,
|
||||
customAudioPath: _customAudioPath,
|
||||
buttons: _buttons.AsReadOnly(),
|
||||
handler: handler,
|
||||
expiration: _expiration,
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ namespace Notify.NET.Extensions
|
|||
private static INotificationService CreateService(NotificationOptions opts)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return new WindowsNotificationService(opts.AppName, opts.AppUserModelId);
|
||||
return new WindowsNotificationService(opts.AppName, opts.AppUserModelId, opts.AppIconPath);
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
return new LinuxNotificationService(opts.AppName);
|
||||
|
|
@ -95,6 +95,14 @@ namespace Notify.NET.Extensions
|
|||
/// </summary>
|
||||
public string AppUserModelId { get; set; } =
|
||||
System.Diagnostics.Process.GetCurrentProcess().ProcessName;
|
||||
|
||||
/// <summary>
|
||||
/// Optional absolute path to an .ico (or .exe/.dll) file whose first icon is stamped onto
|
||||
/// the Start-Menu shortcut and shown as the small icon in the top-left corner of every toast
|
||||
/// notification from this app. Set once at startup; null uses the host executable's icon.
|
||||
/// Windows only — ignored on Linux and macOS.
|
||||
/// </summary>
|
||||
public string? AppIconPath { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -43,15 +43,21 @@ namespace Notify.NET.Platform.Windows
|
|||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
internal struct WNT_ToastDescriptor
|
||||
{
|
||||
public IntPtr title; // wchar_t*
|
||||
public IntPtr body; // wchar_t* (may be IntPtr.Zero)
|
||||
public IntPtr imagePath; // wchar_t* (may be IntPtr.Zero) — square thumbnail
|
||||
public IntPtr heroImagePath; // wchar_t* (may be IntPtr.Zero) — full-width, aspect ratio preserved
|
||||
public IntPtr buttonLabels; // wchar_t** (array of pointers, may be IntPtr.Zero)
|
||||
public IntPtr title; // wchar_t*
|
||||
public IntPtr body; // wchar_t* (may be IntPtr.Zero)
|
||||
public IntPtr imagePath; // wchar_t* (may be IntPtr.Zero) — app logo override in generic templates
|
||||
public IntPtr heroImagePath; // wchar_t* (may be IntPtr.Zero) — full-width banner above notification
|
||||
public IntPtr buttonLabels; // wchar_t** (array of pointers, may be IntPtr.Zero)
|
||||
public int buttonCount;
|
||||
public long expirationMs; // 0 = platform default
|
||||
public int scenario; // WNT_Scenario enum value
|
||||
public int audioOption; // WNT_AudioOption enum value
|
||||
public long expirationMs; // 0 = platform default
|
||||
public int scenario; // WNT_Scenario enum value
|
||||
public int audioOption; // WNT_AudioOption enum value
|
||||
// Extended fields (v2):
|
||||
public IntPtr inlineImagePath; // wchar_t* (may be IntPtr.Zero) — image shown inline in notification body
|
||||
public IntPtr attributionText; // wchar_t* (may be IntPtr.Zero) — small text at the bottom
|
||||
public IntPtr customAudioPath; // wchar_t* (may be IntPtr.Zero) — ms-winsoundevent: or file URI; overrides audioFile
|
||||
public int cropHint; // WNT_CropHint (0 = Square, 1 = Circle)
|
||||
public int audioFile; // WNT_AudioFile (-1 = not set)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -68,9 +74,9 @@ namespace Notify.NET.Platform.Windows
|
|||
}
|
||||
|
||||
// WNT_Scenario values (must match enum in WinToastWrapper.h)
|
||||
internal const int WNT_SCENARIO_DEFAULT = 0;
|
||||
internal const int WNT_SCENARIO_ALARM = 1;
|
||||
internal const int WNT_SCENARIO_REMINDER = 2;
|
||||
internal const int WNT_SCENARIO_DEFAULT = 0;
|
||||
internal const int WNT_SCENARIO_ALARM = 1;
|
||||
internal const int WNT_SCENARIO_REMINDER = 2;
|
||||
internal const int WNT_SCENARIO_INCOMING_CALL = 3;
|
||||
|
||||
// WNT_AudioOption values (must match enum in WinToastWrapper.h)
|
||||
|
|
@ -78,6 +84,19 @@ namespace Notify.NET.Platform.Windows
|
|||
internal const int WNT_AUDIO_SILENT = 1;
|
||||
internal const int WNT_AUDIO_LOOP = 2;
|
||||
|
||||
// WNT_CropHint values (must match enum in WinToastWrapper.h)
|
||||
internal const int WNT_CROP_HINT_SQUARE = 0;
|
||||
internal const int WNT_CROP_HINT_CIRCLE = 1;
|
||||
|
||||
// WNT_AudioFile values (must match enum in WinToastWrapper.h)
|
||||
internal const int WNT_AUDIO_FILE_NONE = -1;
|
||||
internal const int WNT_AUDIO_FILE_DEFAULT = 0;
|
||||
internal const int WNT_AUDIO_FILE_IM = 1;
|
||||
internal const int WNT_AUDIO_FILE_MAIL = 2;
|
||||
internal const int WNT_AUDIO_FILE_REMINDER = 3;
|
||||
internal const int WNT_AUDIO_FILE_SMS = 4;
|
||||
internal const int WNT_AUDIO_FILE_ALARM = 5;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Exported functions
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -93,7 +112,7 @@ namespace Notify.NET.Platform.Windows
|
|||
/// <returns>true on success.</returns>
|
||||
[DllImport(DllName, EntryPoint = "WNT_Initialize", CharSet = CharSet.Unicode, SetLastError = false)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool WNT_Initialize(string appName, string appUserModelId);
|
||||
internal static extern bool WNT_Initialize(string appName, string appUserModelId, string? appIconPath);
|
||||
|
||||
/// <summary>Uninitialises WinToastLib and releases all internal resources.</summary>
|
||||
[DllImport(DllName, EntryPoint = "WNT_Uninitialize")]
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ namespace Notify.NET.Platform.Windows
|
|||
{
|
||||
private readonly string _appName;
|
||||
private readonly string _appUserModelId;
|
||||
private readonly string? _appIconPath;
|
||||
|
||||
private readonly Thread _staThread;
|
||||
private readonly BlockingCollection<Action> _workQueue = new BlockingCollection<Action>();
|
||||
|
|
@ -40,10 +41,16 @@ namespace Notify.NET.Platform.Windows
|
|||
/// A Start-Menu shortcut carrying this AUMI is required for notifications to persist in
|
||||
/// the Action Centre. The native wrapper creates the shortcut automatically when missing.
|
||||
/// </param>
|
||||
public WindowsNotificationService(string appName, string appUserModelId)
|
||||
/// <param name="appIconPath">
|
||||
/// Optional absolute path to an .ico (or .exe/.dll) file whose first icon is used as the
|
||||
/// small icon in the top-left corner of every toast notification from this app.
|
||||
/// Pass null to use the host executable's default icon.
|
||||
/// </param>
|
||||
public WindowsNotificationService(string appName, string appUserModelId, string? appIconPath = null)
|
||||
{
|
||||
_appName = appName ?? throw new ArgumentNullException(nameof(appName));
|
||||
_appUserModelId = appUserModelId ?? throw new ArgumentNullException(nameof(appUserModelId));
|
||||
_appIconPath = appIconPath;
|
||||
|
||||
_staThread = new Thread(StaThreadProc)
|
||||
{
|
||||
|
|
@ -171,7 +178,7 @@ namespace Notify.NET.Platform.Windows
|
|||
return;
|
||||
}
|
||||
|
||||
bool ok = WinToastNative.WNT_Initialize(_appName, _appUserModelId);
|
||||
bool ok = WinToastNative.WNT_Initialize(_appName, _appUserModelId, _appIconPath);
|
||||
if (!ok)
|
||||
{
|
||||
_initException = new NotificationException("WNT_Initialize returned false.");
|
||||
|
|
@ -214,10 +221,13 @@ namespace Notify.NET.Platform.Windows
|
|||
// Pin managed strings as unmanaged UTF-16 memory for the duration of the call.
|
||||
// button label pointers are pinned in the IntPtr[] and that array is pinned too.
|
||||
|
||||
using var titlePin = new PinnedString(request.Title);
|
||||
using var bodyPin = new PinnedString(request.Body);
|
||||
using var imagePin = new PinnedString(ResolveImagePath(request.ImagePath));
|
||||
using var heroImagePin = new PinnedString(ResolveImagePath(request.HeroImagePath));
|
||||
using var titlePin = new PinnedString(request.Title);
|
||||
using var bodyPin = new PinnedString(request.Body);
|
||||
using var imagePin = new PinnedString(ResolveImagePath(request.ImagePath));
|
||||
using var heroImagePin = new PinnedString(ResolveImagePath(request.HeroImagePath));
|
||||
using var inlineImagePin = new PinnedString(ResolveImagePath(request.InlineImagePath));
|
||||
using var attributionPin = new PinnedString(request.AttributionText);
|
||||
using var customAudioPin = new PinnedString(request.CustomAudioPath);
|
||||
|
||||
// Build array of pinned button label pointers.
|
||||
var buttonPins = new PinnedString[request.Buttons.Count];
|
||||
|
|
@ -242,15 +252,24 @@ namespace Notify.NET.Platform.Windows
|
|||
|
||||
var descriptor = new WinToastNative.WNT_ToastDescriptor
|
||||
{
|
||||
title = titlePin.Pointer,
|
||||
body = bodyPin.Pointer,
|
||||
imagePath = imagePin.Pointer,
|
||||
heroImagePath = heroImagePin.Pointer,
|
||||
buttonLabels = buttonArrayPtr,
|
||||
buttonCount = request.Buttons.Count,
|
||||
expirationMs = request.Expiration.HasValue ? (long)request.Expiration.Value.TotalMilliseconds : 0L,
|
||||
scenario = MapScenario(request.Urgency),
|
||||
audioOption = MapAudio(request.Audio)
|
||||
title = titlePin.Pointer,
|
||||
body = bodyPin.Pointer,
|
||||
imagePath = imagePin.Pointer,
|
||||
heroImagePath = heroImagePin.Pointer,
|
||||
buttonLabels = buttonArrayPtr,
|
||||
buttonCount = request.Buttons.Count,
|
||||
expirationMs = request.Expiration.HasValue ? (long)request.Expiration.Value.TotalMilliseconds : 0L,
|
||||
scenario = MapScenario(request.Urgency),
|
||||
audioOption = MapAudio(request.Audio),
|
||||
inlineImagePath = inlineImagePin.Pointer,
|
||||
attributionText = attributionPin.Pointer,
|
||||
customAudioPath = customAudioPin.Pointer,
|
||||
cropHint = request.ImageCropHint == NotificationImageCropHint.Circle
|
||||
? WinToastNative.WNT_CROP_HINT_CIRCLE
|
||||
: WinToastNative.WNT_CROP_HINT_SQUARE,
|
||||
audioFile = request.AudioFile.HasValue
|
||||
? (int)request.AudioFile.Value
|
||||
: WinToastNative.WNT_AUDIO_FILE_NONE
|
||||
};
|
||||
|
||||
var handler = new WinToastNative.WNT_Handler
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue