Cleanup & intro OSX

This commit is contained in:
Luis von der Eltz
2021-01-31 15:40:50 +01:00
commit e13f197ff0
20 changed files with 1620 additions and 0 deletions
+527
View File
@@ -0,0 +1,527 @@
# Created by https://www.toptal.com/developers/gitignore/api/visualstudio,dotnetcore,csharp
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio,dotnetcore,csharp
### Csharp ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*[.json, .xml, .info]
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
### DotnetCore ###
# .NET Core build folders
bin/
obj/
# Common node modules locations
/node_modules
/wwwroot/node_modules
### VisualStudio ###
# User-specific files
# User-specific files (MonoDevelop/Xamarin Studio)
# Mono auto generated files
# Build results
# Visual Studio 2015/2017 cache/options directory
# Uncomment if you have tasks that create the project's static files in wwwroot
# Visual Studio 2017 auto generated files
# MSTest test Results
# NUnit
# Build Results of an ATL Project
# Benchmark Results
# .NET Core
# StyleCop
# Files built by Visual Studio
# Chutzpah Test files
# Visual C++ cache files
# Visual Studio profiler
# Visual Studio Trace Files
# TFS 2012 Local Workspace
# Guidance Automation Toolkit
# ReSharper is a .NET coding add-in
# TeamCity is a build add-in
# DotCover is a Code Coverage Tool
# AxoCover is a Code Coverage Tool
# Coverlet is a free, cross platform Code Coverage Tool
# Visual Studio code coverage results
# NCrunch
# MightyMoose
# Web workbench (sass)
# Installshield output folder
# DocProject is a documentation generator add-in
# Click-Once directory
# Publish Web Output
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
# NuGet Packages
# NuGet Symbol Packages
# The packages folder can be ignored because of Package Restore
# except build/, which is used as an MSBuild target.
# Uncomment if necessary however generally it will be regenerated when needed
# NuGet v3's project.json files produces more ignorable files
# Microsoft Azure Build Output
# Microsoft Azure Emulator
# Windows Store app package directories and files
# Visual Studio cache files
# files ending in .cache can be ignored
# but keep track of directories ending in .cache
# Others
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
# RIA/Silverlight projects
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
# SQL Server files
# Business Intelligence projects
# Microsoft Fakes
# GhostDoc plugin setting file
# Node.js Tools for Visual Studio
# Visual Studio 6 build log
# Visual Studio 6 workspace options file
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
# Visual Studio LightSwitch build output
# Paket dependency manager
# FAKE - F# Make
# CodeRush personal settings
# Python Tools for Visual Studio (PTVS)
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
# Telerik's JustMock configuration file
# BizTalk build output
# OpenCover UI analysis results
# Azure Stream Analytics local run output
# MSBuild Binary and Structured Log
# NVidia Nsight GPU debugger configuration file
# MFractors (Xamarin productivity tool) working folder
# Local History for Visual Studio
# BeatPulse healthcheck temp database
# Backup folder for Package Reference Convert tool in Visual Studio 2017
# Ionide (cross platform F# VS Code tools) working folder
# End of https://www.toptal.com/developers/gitignore/api/visualstudio,dotnetcore,csharp
@@ -0,0 +1,23 @@
using System;
using System.Threading.Tasks;
namespace DesktopNotifications.Apple
{
public class AppleNotificationManager : INotificationManager
{
public void Dispose()
{
}
public event EventHandler<NotificationActivatedEventArgs> NotificationActivated;
public event EventHandler<NotificationDismissedEventArgs> NotificationDismissed;
public ValueTask Initialize()
{
}
public ValueTask ShowNotification(Notification notification, DateTimeOffset? expirationTime = null)
{
}
}
}
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DesktopNotifications\DesktopNotifications.csproj" />
</ItemGroup>
</Project>
+7
View File
@@ -0,0 +1,7 @@
void __declspec(dllexport) ShowNotification(char* title, char* body) {
}
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Tmds.DBus" Version="0.9.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DesktopNotifications\DesktopNotifications.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Tmds.DBus;
namespace DesktopNotifications.FreeDesktop
{
public class FreeDesktopNotificationManager : INotificationManager, IDisposable
{
private const string NotificationsService = "org.freedesktop.Notifications";
private static readonly ObjectPath NotificationsPath = new ObjectPath("/org/freedesktop/Notifications");
private Connection? _connection;
private IDisposable? _notificationActionSubscription;
private IDisposable? _notificationCloseSubscription;
private Dictionary<uint, Notification> _activeNotifications;
private IFreeDesktopNotificationsProxy? _proxy;
public FreeDesktopNotificationManager()
{
_activeNotifications = new Dictionary<uint, Notification>();
}
public void Dispose()
{
_notificationActionSubscription?.Dispose();
_notificationCloseSubscription?.Dispose();
}
public event EventHandler<NotificationActivatedEventArgs>? NotificationActivated;
public event EventHandler<NotificationDismissedEventArgs>? NotificationDismissed;
public async ValueTask Initialize()
{
_connection = Connection.Session;
await _connection.ConnectAsync();
_proxy = _connection.CreateProxy<IFreeDesktopNotificationsProxy>(
NotificationsService,
NotificationsPath
);
_notificationActionSubscription = await _proxy.WatchActionInvokedAsync(
OnNotificationActionInvoked,
OnNotificationActionInvokedError
);
_notificationCloseSubscription = await _proxy.WatchNotificationClosedAsync(
OnNotificationClosed,
OnNotificationClosedError
);
}
public async ValueTask ShowNotification(Notification notification, DateTimeOffset? expirationTime = null)
{
if (_connection == null || _proxy == null)
{
throw new InvalidOperationException("Not connected. Call Initialize() first.");
}
var duration = expirationTime - DateTimeOffset.Now;
var actions = GenerateActions(notification);
var id = await _proxy.NotifyAsync(
"MyApp",
0,
string.Empty,
notification.Title,
notification.Body,
actions.ToArray(),
new Dictionary<string, object> {{"urgency", 1}},
duration?.Milliseconds ?? 0
).ConfigureAwait(false);
_activeNotifications[id] = notification;
}
private static IEnumerable<string> GenerateActions(Notification notification)
{
foreach (var (title, actionId) in notification.Buttons)
{
yield return actionId;
yield return title;
}
}
private void OnNotificationClosedError(Exception obj)
{
throw obj;
}
private static NotificationDismissReason GetReason(uint reason)
{
return reason switch
{
1 => NotificationDismissReason.Expired,
2 => NotificationDismissReason.User,
3 => NotificationDismissReason.Application,
_ => throw new ArgumentOutOfRangeException()
};
}
private void OnNotificationClosed((uint id, uint reason) @event)
{
_activeNotifications.Remove(@event.id, out var notification);
Debug.Assert(notification != null);
var dismissReason = GetReason(@event.reason);
NotificationDismissed?.Invoke(this,
new NotificationDismissedEventArgs(notification, dismissReason));
}
private void OnNotificationActionInvokedError(Exception obj)
{
throw obj;
}
private void OnNotificationActionInvoked((uint id, string actionKey) @event)
{
var notification = _activeNotifications[@event.id];
NotificationActivated?.Invoke(this,
new NotificationActivatedEventArgs(notification, @event.actionKey));
}
}
}
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Tmds.DBus;
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
namespace DesktopNotifications.FreeDesktop
{
/// <seealso>http://www.galago-project.org/specs/notification/0.9/x408.html</seealso>
/// <summary>
/// Interface for notifications
/// </summary>
[DBusInterface("org.freedesktop.Notifications")]
internal interface IFreeDesktopNotificationsProxy : IDBusObject
{
Task<uint> NotifyAsync(string appName, uint replacesId, string appIcon, string summary, string body, string[] actions, IDictionary<string, object> hints, int expireTimeout);
Task CloseNotificationAsync(uint id);
Task<string[]> GetCapabilitiesAsync();
Task<(string name, string vendor, string version, string spec_version)> GetServerInformationAsync();
Task<IDisposable> WatchNotificationClosedAsync(Action<(uint id, uint reason)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchActionInvokedAsync(Action<(uint id, string action_key)> handler, Action<Exception> onError = null);
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DesktopNotifications\DesktopNotifications.csproj" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.0.0-preview4" />
</ItemGroup>
</Project>
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.Data.Xml.Dom;
using Windows.UI.Notifications;
using Microsoft.Toolkit.Uwp.Notifications;
namespace DesktopNotifications.Windows
{
public class WindowsNotificationManager : INotificationManager
{
private readonly Dictionary<ToastNotification, Notification> _notifications;
private readonly ToastNotifier _toastNotifier;
private TaskCompletionSource<string?>? _launchActionPromise;
/// <summary>
/// </summary>
/// <param name="appId"></param>
public WindowsNotificationManager(string appId)
{
_toastNotifier = ToastNotificationManager.CreateToastNotifier(appId);
_notifications = new Dictionary<ToastNotification, Notification>();
}
public event EventHandler<NotificationActivatedEventArgs>? NotificationActivated;
public event EventHandler<NotificationDismissedEventArgs>? NotificationDismissed;
public async ValueTask Initialize()
{
if (ToastNotificationManagerCompat.WasCurrentProcessToastActivated())
{
_launchActionPromise = new TaskCompletionSource<string?>();
ToastNotificationManagerCompat.OnActivated += OnAppActivated;
var launchAction = await _launchActionPromise.Task;
Debug.Assert(launchAction != null);
//TODO: Lookup notification object from history?
NotificationActivated?.Invoke(this,
new NotificationActivatedEventArgs(null, launchAction));
}
}
private static XmlDocument GenerateXml(Notification notification)
{
var builder = new ToastContentBuilder();
builder.AddText(notification.Title);
builder.AddText(notification.Body);
foreach (var (title, actionId) in notification.Buttons)
{
builder.AddButton(title, ToastActivationType.Foreground, actionId);
}
return builder.GetXml();
}
public ValueTask ShowNotification(Notification notification, DateTimeOffset? expirationTime)
{
if (expirationTime < DateTimeOffset.Now)
{
throw new ArgumentException(nameof(expirationTime));
}
var xmlContent = GenerateXml(notification);
var toastNotification = new ToastNotification(xmlContent)
{
ExpirationTime = expirationTime
};
toastNotification.Activated += ToastNotificationOnActivated;
toastNotification.Dismissed += ToastNotificationOnDismissed;
toastNotification.Failed += ToastNotificationOnFailed;
_toastNotifier.Show(toastNotification);
_notifications[toastNotification] = notification;
return default;
}
private void OnAppActivated(ToastNotificationActivatedEventArgsCompat e)
{
Debug.Assert(_launchActionPromise != null);
_launchActionPromise.SetResult(e.Argument);
}
private static void ToastNotificationOnFailed(ToastNotification sender, ToastFailedEventArgs args)
{
throw args.ErrorCode;
}
private void ToastNotificationOnDismissed(ToastNotification sender, ToastDismissedEventArgs args)
{
var notification = _notifications[sender];
var reason = args.Reason switch
{
ToastDismissalReason.UserCanceled => NotificationDismissReason.User,
ToastDismissalReason.TimedOut => NotificationDismissReason.Expired,
ToastDismissalReason.ApplicationHidden => NotificationDismissReason.Application,
_ => throw new ArgumentOutOfRangeException()
};
NotificationDismissed?.Invoke(this, new NotificationDismissedEventArgs(notification, reason));
_notifications.Remove(sender);
}
private void ToastNotificationOnActivated(ToastNotification sender, object args)
{
var activationArgs = (ToastActivatedEventArgs) args;
var notification = _notifications[sender];
var actionId = string.IsNullOrEmpty(activationArgs.Arguments) ? "default" : activationArgs.Arguments;
NotificationActivated?.Invoke(this, new NotificationActivatedEventArgs(notification, actionId));
}
public void Dispose()
{
}
}
}
+49
View File
@@ -0,0 +1,49 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30804.86
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DesktopNotifications", "DesktopNotifications\DesktopNotifications.csproj", "{64E5A8ED-29CB-4114-B1B8-EC14512EB15E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DesktopNotifications.Windows", "DesktopNotifications.Windows\DesktopNotifications.Windows.csproj", "{8BD34395-0FB7-43D1-8127-94B525AF2392}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example", "Example\Example.csproj", "{CEB162D5-CC75-4FC9-9A8B-E3C58E7155F0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DesktopNotifications.FreeDesktop", "DesktopNotifications.FreeDesktop\DesktopNotifications.FreeDesktop.csproj", "{96F637EF-FD11-41F5-8F40-4E37313697E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DesktopNotifications.Apple", "DesktopNotifications.Apple\DesktopNotifications.Apple.csproj", "{B4A6C639-79C4-48EF-955F-273CF01D7770}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{64E5A8ED-29CB-4114-B1B8-EC14512EB15E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{64E5A8ED-29CB-4114-B1B8-EC14512EB15E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{64E5A8ED-29CB-4114-B1B8-EC14512EB15E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{64E5A8ED-29CB-4114-B1B8-EC14512EB15E}.Release|Any CPU.Build.0 = Release|Any CPU
{8BD34395-0FB7-43D1-8127-94B525AF2392}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8BD34395-0FB7-43D1-8127-94B525AF2392}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BD34395-0FB7-43D1-8127-94B525AF2392}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BD34395-0FB7-43D1-8127-94B525AF2392}.Release|Any CPU.Build.0 = Release|Any CPU
{CEB162D5-CC75-4FC9-9A8B-E3C58E7155F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CEB162D5-CC75-4FC9-9A8B-E3C58E7155F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEB162D5-CC75-4FC9-9A8B-E3C58E7155F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CEB162D5-CC75-4FC9-9A8B-E3C58E7155F0}.Release|Any CPU.Build.0 = Release|Any CPU
{96F637EF-FD11-41F5-8F40-4E37313697E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{96F637EF-FD11-41F5-8F40-4E37313697E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{96F637EF-FD11-41F5-8F40-4E37313697E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{96F637EF-FD11-41F5-8F40-4E37313697E3}.Release|Any CPU.Build.0 = Release|Any CPU
{B4A6C639-79C4-48EF-955F-273CF01D7770}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B4A6C639-79C4-48EF-955F-273CF01D7770}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B4A6C639-79C4-48EF-955F-273CF01D7770}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B4A6C639-79C4-48EF-955F-273CF01D7770}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {95891D52-BFE3-4A10-9823-99F37EAA6058}
EndGlobalSection
EndGlobal
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
@@ -0,0 +1,34 @@
using System;
using System.Threading.Tasks;
namespace DesktopNotifications
{
/// <summary>
/// Interface for notification managers that handle the presentation and lifetime of notifications.
/// </summary>
public interface INotificationManager : IDisposable
{
/// <summary>
/// Raised when a notification was activated. The notion of "activation" varies from platform to platform.
/// </summary>
event EventHandler<NotificationActivatedEventArgs> NotificationActivated;
/// <summary>
/// Raised when a notification was dismissed. The exact reason can be found in <see cref="NotificationDismissedEventArgs"/>.
/// </summary>
event EventHandler<NotificationDismissedEventArgs> NotificationDismissed;
/// <summary>
///
/// </summary>
/// <returns></returns>
ValueTask Initialize();
/// <summary>
/// Schedules a notification for presentation.
/// </summary>
/// <param name="notification">The notification to present.</param>
/// <param name="expirationTime">The expiration time marking the point when the notification gets removed.</param>
ValueTask ShowNotification(Notification notification, DateTimeOffset? expirationTime = null);
}
}
+20
View File
@@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace DesktopNotifications
{
/// <summary>
/// </summary>
public class Notification
{
public Notification()
{
Buttons = new List<(string Title, string ActionId)>();
}
public string Title { get; set; }
public string Body { get; set; }
public List<(string Title, string ActionId)> Buttons { get; }
}
}
@@ -0,0 +1,20 @@
namespace DesktopNotifications
{
/// <summary>
///
/// </summary>
public class NotificationActivatedEventArgs : NotificationEventArgs
{
public NotificationActivatedEventArgs(Notification notification, string actionId)
: base(notification)
{
ActionId = actionId;
}
/// <summary>
/// The id associated with the activation action. "default" denotes the platform-specific default action.
/// On Windows this means the user clicked on the notification.
/// </summary>
public string ActionId { get; }
}
}
@@ -0,0 +1,23 @@
namespace DesktopNotifications
{
/// <summary>
/// Reasons why a notification was dismissed.
/// </summary>
public enum NotificationDismissReason
{
/// <summary>
/// The user closed the notification.
/// </summary>
User,
/// <summary>
/// The notification expired.
/// </summary>
Expired,
/// <summary>
/// The notification was explicitly removed by application code.
/// </summary>
Application
}
}
@@ -0,0 +1,16 @@
namespace DesktopNotifications
{
/// <summary>
/// </summary>
public class NotificationDismissedEventArgs
{
public NotificationDismissedEventArgs(Notification notification, NotificationDismissReason reason)
{
Notification = notification;
Reason = reason;
}
public Notification Notification { get; }
public NotificationDismissReason Reason { get; }
}
}
@@ -0,0 +1,17 @@
namespace DesktopNotifications
{
/// <summary>
/// </summary>
public class NotificationEventArgs
{
public NotificationEventArgs(Notification notification)
{
Notification = notification;
}
/// <summary>
///
/// </summary>
public Notification Notification { get; }
}
}
+15
View File
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DesktopNotifications.FreeDesktop\DesktopNotifications.FreeDesktop.csproj" />
<ProjectReference Include="..\DesktopNotifications.Windows\DesktopNotifications.Windows.csproj" />
<ProjectReference Include="..\DesktopNotifications\DesktopNotifications.csproj" />
</ItemGroup>
</Project>
+87
View File
@@ -0,0 +1,87 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using DesktopNotifications;
using DesktopNotifications.FreeDesktop;
using DesktopNotifications.Windows;
using Example.Win32;
namespace Example
{
internal class Program
{
[DllImport("shell32.dll", SetLastError = true)]
internal static extern void SetCurrentProcessExplicitAppUserModelID(
[MarshalAs(UnmanagedType.LPWStr)] string appId);
private static INotificationManager CreateManager()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return new FreeDesktopNotificationManager();
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
const string AppName = "DesktopNotificationsExample";
SetCurrentProcessExplicitAppUserModelID(AppName);
using var shortcut = new ShellLink
{
TargetPath = Process.GetCurrentProcess().MainModule.FileName,
Arguments = string.Empty,
AppUserModelID = AppName
};
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var startMenuPath = Path.Combine(appData, @"Microsoft\Windows\Start Menu\Programs");
var shortcutFile = Path.Combine(startMenuPath, $"{AppName}.lnk");
shortcut.Save(shortcutFile);
return new WindowsNotificationManager(AppName);
}
throw new PlatformNotSupportedException();
}
private static async Task Main(string[] args)
{
using var manager = CreateManager();
manager.NotificationActivated += ManagerOnNotificationActivated;
manager.NotificationDismissed += ManagerOnNotificationDismissed;
await manager.Initialize();
var notification = new Notification
{
Title = "Hello World!",
Body = "Isn't this awesome?",
Buttons =
{
("Yes", "answer_yes"),
("No", "answer_no"),
("Maybe", "answer_maybe")
}
};
await manager.ShowNotification(notification);
await Task.Delay(10_000);
}
private static void ManagerOnNotificationDismissed(object? sender, NotificationDismissedEventArgs e)
{
Console.WriteLine($"Notification dismissed: {e.Reason}");
}
private static void ManagerOnNotificationActivated(object? sender, NotificationActivatedEventArgs e)
{
Console.WriteLine($"Notification activated: {e.ActionId}");
}
}
}
+449
View File
@@ -0,0 +1,449 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
namespace Example.Win32
{
// Modified from http://smdn.jp/programming/tips/createlnk/
// Originally from http://www.vbaccelerator.com/home/NET/Code/Libraries/Shell_Projects
// /Creating_and_Modifying_Shortcuts/article.asp
// Partly based on Sending toast notifications from desktop apps sample
public class ShellLink : IDisposable
{
#region Win32 and COM
// IShellLink Interface
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
private interface IShellLinkW
{
uint GetPath([Out] [MarshalAs(UnmanagedType.LPWStr)]
StringBuilder pszFile,
int cchMaxPath, ref WIN32_FIND_DATAW pfd, uint fFlags);
uint GetIDList(out IntPtr ppidl);
uint SetIDList(IntPtr pidl);
uint GetDescription([Out] [MarshalAs(UnmanagedType.LPWStr)]
StringBuilder pszName,
int cchMaxName);
uint SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
uint GetWorkingDirectory([Out] [MarshalAs(UnmanagedType.LPWStr)]
StringBuilder pszDir,
int cchMaxPath);
uint SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
uint GetArguments([Out] [MarshalAs(UnmanagedType.LPWStr)]
StringBuilder pszArgs,
int cchMaxPath);
uint SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
uint GetHotKey(out ushort pwHotkey);
uint SetHotKey(ushort wHotKey);
uint GetShowCmd(out int piShowCmd);
uint SetShowCmd(int iShowCmd);
uint GetIconLocation([Out] [MarshalAs(UnmanagedType.LPWStr)]
StringBuilder pszIconPath,
int cchIconPath, out int piIcon);
uint SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
uint SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel,
uint dwReserved);
uint Resolve(IntPtr hwnd, uint fFlags);
uint SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
// ShellLink CoClass (ShellLink object)
[ComImport]
[ClassInterface(ClassInterfaceType.None)]
[Guid("00021401-0000-0000-C000-000000000046")]
private class CShellLink
{
}
// WIN32_FIND_DATAW Structure
[StructLayout(LayoutKind.Sequential, Pack = 4, CharSet = CharSet.Unicode)]
private struct WIN32_FIND_DATAW
{
public readonly uint dwFileAttributes;
public readonly FILETIME ftCreationTime;
public readonly FILETIME ftLastAccessTime;
public readonly FILETIME ftLastWriteTime;
public readonly uint nFileSizeHigh;
public readonly uint nFileSizeLow;
public readonly uint dwReserved0;
public readonly uint dwReserved1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PATH)]
public readonly string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
public readonly string cAlternateFileName;
}
// IPropertyStore Interface
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
private interface IPropertyStore
{
uint GetCount([Out] out uint cProps);
uint GetAt([In] uint iProp, out PropertyKey pkey);
uint GetValue([In] ref PropertyKey key, [Out] PropVariant pv);
uint SetValue([In] ref PropertyKey key, [In] PropVariant pv);
uint Commit();
}
// PropertyKey Structure
// Narrowed down from PropertyKey.cs of Windows API Code Pack 1.1
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct PropertyKey
{
#region Fields
#endregion
#region Public Properties
public Guid FormatId { get; }
public int PropertyId { get; }
#endregion
#region Constructor
public PropertyKey(Guid formatId, int propertyId)
{
FormatId = formatId;
PropertyId = propertyId;
}
public PropertyKey(string formatId, int propertyId)
{
FormatId = new Guid(formatId);
PropertyId = propertyId;
}
#endregion
}
// PropVariant Class (only for string value)
// Narrowed down from PropVariant.cs of Windows API Code Pack 1.1
// Originally from http://blogs.msdn.com/b/adamroot/archive/2008/04/11
// /interop-with-propvariants-in-net.aspx
[StructLayout(LayoutKind.Explicit)]
private sealed class PropVariant : IDisposable
{
#region Fields
[FieldOffset(0)] private ushort valueType; // Value type
// [FieldOffset(2)]
// ushort wReserved1; // Reserved field
// [FieldOffset(4)]
// ushort wReserved2; // Reserved field
// [FieldOffset(6)]
// ushort wReserved3; // Reserved field
[FieldOffset(8)] private readonly IntPtr ptr; // Value
#endregion
#region Public Properties
// Value type (System.Runtime.InteropServices.VarEnum)
public VarEnum VarType
{
get => (VarEnum) valueType;
set => valueType = (ushort) value;
}
// Whether value is empty or null
public bool IsNullOrEmpty =>
valueType == (ushort) VarEnum.VT_EMPTY ||
valueType == (ushort) VarEnum.VT_NULL;
// Value (only for string value)
public string? Value => Marshal.PtrToStringUni(ptr);
#endregion
#region Constructor
public PropVariant()
{
}
// Construct with string value
public PropVariant(string value)
{
if (value == null)
{
throw new ArgumentException("Failed to set value.");
}
valueType = (ushort) VarEnum.VT_LPWSTR;
ptr = Marshal.StringToCoTaskMemUni(value);
}
#endregion
#region Destructor
~PropVariant()
{
Dispose();
}
public void Dispose()
{
PropVariantClear(this);
GC.SuppressFinalize(this);
}
#endregion
}
[DllImport("Ole32.dll", PreserveSig = false)]
private static extern void PropVariantClear([In] [Out] PropVariant pvar);
#endregion
#region Fields
private IShellLinkW? shellLinkW;
// Name = System.AppUserModel.ID
// ShellPKey = PKEY_AppUserModel_ID
// FormatID = 9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3
// PropID = 5
// Type = String (VT_LPWSTR)
private readonly PropertyKey AppUserModelIDKey =
new PropertyKey("{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}", 5);
private const int MAX_PATH = 260;
private const int INFOTIPSIZE = 1024;
private const int STGM_READ = 0x00000000; // STGM constants
private const uint SLGP_UNCPRIORITY = 0x0002; // SLGP flags
#endregion
#region Private Properties (Interfaces)
private IPersistFile PersistFile
{
get
{
if (!(shellLinkW is IPersistFile persistFile))
{
throw new COMException("Failed to create IPersistFile.");
}
return persistFile;
}
}
private IPropertyStore PropertyStore
{
get
{
if (!(shellLinkW is IPropertyStore PropertyStore))
{
throw new COMException("Failed to create IPropertyStore.");
}
return PropertyStore;
}
}
#endregion
#region Public Properties (Minimal)
// Path of loaded shortcut file
public string ShortcutFile
{
get
{
string shortcutFile;
PersistFile.GetCurFile(out shortcutFile);
return shortcutFile;
}
}
// Path of target file
public string TargetPath
{
get
{
// No limitation to length of buffer string in the case of Unicode though.
StringBuilder targetPath = new StringBuilder(MAX_PATH);
var data = new WIN32_FIND_DATAW();
VerifySucceeded(shellLinkW!.GetPath(targetPath, targetPath.Capacity, ref data,
SLGP_UNCPRIORITY));
return targetPath.ToString();
}
set => VerifySucceeded(shellLinkW!.SetPath(value));
}
public string Arguments
{
get
{
// No limitation to length of buffer string in the case of Unicode though.
StringBuilder arguments = new StringBuilder(INFOTIPSIZE);
VerifySucceeded(shellLinkW!.GetArguments(arguments, arguments.Capacity));
return arguments.ToString();
}
set => VerifySucceeded(shellLinkW!.SetArguments(value));
}
// AppUserModelID to be used for Windows 7 or later.
public string AppUserModelID
{
get
{
using (PropVariant pv = new PropVariant())
{
VerifySucceeded(PropertyStore.GetValue(AppUserModelIDKey, pv));
if (pv.Value == null)
{
return "Null";
}
return pv.Value;
}
}
set
{
using (PropVariant pv = new PropVariant(value))
{
VerifySucceeded(PropertyStore.SetValue(AppUserModelIDKey, pv));
VerifySucceeded(PropertyStore.Commit());
}
}
}
#endregion
#region Constructor
public ShellLink()
: this(null)
{
}
// Construct with loading shortcut file.
public ShellLink(string? file)
{
try
{
shellLinkW = (IShellLinkW) new CShellLink();
}
catch
{
throw new COMException("Failed to create ShellLink object.");
}
if (file != null)
{
Load(file);
}
}
#endregion
#region Destructor
~ShellLink()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (shellLinkW != null)
{
// Release all references.
Marshal.FinalReleaseComObject(shellLinkW);
shellLinkW = null;
}
}
#endregion
#region Methods
// Save shortcut file.
public void Save()
{
string file = ShortcutFile;
if (file == null)
{
throw new InvalidOperationException("File name is not given.");
}
Save(file);
}
public void Save(string file)
{
if (file == null)
{
throw new ArgumentNullException(nameof(file));
}
PersistFile.Save(file, true);
}
// Load shortcut file.
public void Load(string file)
{
if (!File.Exists(file))
{
throw new FileNotFoundException("File is not found.", file);
}
PersistFile.Load(file, STGM_READ);
}
// Verify if operation succeeded.
public static void VerifySucceeded(uint hresult)
{
if (hresult > 1)
{
throw new InvalidOperationException("Failed with HRESULT: " +
hresult.ToString("X"));
}
}
#endregion
}
}