Cleanup & intro OSX
This commit is contained in:
+527
@@ -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>
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user