Register shortcut for Windows toast notifications

This commit is contained in:
Emby Toolbox 2026-05-16 15:38:05 +05:00
parent 36bbc863f8
commit 6cf86d41f5
3 changed files with 163 additions and 1 deletions

View File

@ -8,9 +8,9 @@ public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
_ = ToastShortcutRegistration.TryEnsureStartMenuShortcut(NotificationService.ToastAppUserModelId);
_ = AppUserModelIdRegistration.TryRegister(NotificationService.ToastAppUserModelId);
base.OnStartup(e);
}
}

View File

@ -0,0 +1,155 @@
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
namespace EmbyToolbox.Interop;
internal static class ToastShortcutRegistration
{
private const int StgmReadwrite = 0x00000002;
private static readonly PropertyKey AppUserModelIdKey = new(
new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"),
5);
public static string? LastDiagnostics { get; private set; }
public static bool TryEnsureStartMenuShortcut(string appUserModelId)
{
LastDiagnostics = null;
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 10240))
{
LastDiagnostics = "требуется Windows 10 (10240) или новее.";
return false;
}
try
{
var exePath = Environment.ProcessPath;
if (string.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath))
{
LastDiagnostics = "не удалось определить путь к exe.";
return false;
}
var programs = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
var shortcutPath = Path.Combine(programs, "Programs", "Emby Toolbox.lnk");
Directory.CreateDirectory(Path.GetDirectoryName(shortcutPath)!);
var shellLinkObject = (object)new CShellLink();
var shellLink = (IShellLinkW)shellLinkObject;
shellLink.SetPath(exePath);
shellLink.SetArguments(string.Empty);
shellLink.SetWorkingDirectory(Path.GetDirectoryName(exePath) ?? AppContext.BaseDirectory);
shellLink.SetDescription("Emby Toolbox");
if (File.Exists(Path.Combine(AppContext.BaseDirectory, "Resources", "AppIcon.ico")))
{
shellLink.SetIconLocation(Path.Combine(AppContext.BaseDirectory, "Resources", "AppIcon.ico"), 0);
}
else
{
shellLink.SetIconLocation(exePath, 0);
}
using var appId = PropVariant.FromString(appUserModelId);
var propertyStore = (IPropertyStore)shellLink;
propertyStore.SetValue(AppUserModelIdKey, appId);
propertyStore.Commit();
var persistFile = (IPersistFile)shellLink;
persistFile.Save(shortcutPath, true);
return true;
}
catch (Exception ex)
{
LastDiagnostics = $"{ex.GetType().Name}: {ex.Message}";
return false;
}
}
[ComImport]
[Guid("00021401-0000-0000-C000-000000000046")]
private sealed class CShellLink
{
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
private interface IShellLinkW
{
void GetPath(IntPtr pszFile, int cchMaxPath, IntPtr pfd, uint fFlags);
void GetIDList(out IntPtr ppidl);
void SetIDList(IntPtr pidl);
void GetDescription(IntPtr pszName, int cchMaxName);
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
void GetWorkingDirectory(IntPtr pszDir, int cchMaxPath);
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
void GetArguments(IntPtr pszArgs, int cchMaxPath);
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
void GetHotkey(out short pwHotkey);
void SetHotkey(short wHotkey);
void GetShowCmd(out int piShowCmd);
void SetShowCmd(int iShowCmd);
void GetIconLocation(IntPtr pszIconPath, int cchIconPath, out int piIcon);
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved);
void Resolve(IntPtr hwnd, uint fFlags);
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00000138-0000-0000-C000-000000000046")]
private interface IPropertyStore
{
void GetCount(out uint cProps);
void GetAt(uint iProp, out PropertyKey pkey);
void GetValue(ref PropertyKey key, out PropVariant pv);
void SetValue(in PropertyKey key, in PropVariant pv);
void Commit();
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private readonly struct PropertyKey(Guid formatId, int propertyId)
{
private readonly Guid _formatId = formatId;
private readonly int _propertyId = propertyId;
}
[StructLayout(LayoutKind.Sequential)]
private sealed class PropVariant : IDisposable
{
private ushort _valueType;
private ushort _reserved1;
private ushort _reserved2;
private ushort _reserved3;
private IntPtr _value;
private IntPtr _reserved4;
public static PropVariant FromString(string value)
{
return new PropVariant
{
_valueType = 31, // VT_LPWSTR
_value = Marshal.StringToCoTaskMemUni(value)
};
}
public void Dispose()
{
PropVariantClear(this);
GC.SuppressFinalize(this);
}
~PropVariant()
{
PropVariantClear(this);
}
[DllImport("ole32.dll")]
private static extern int PropVariantClear([In, Out] PropVariant pvar);
}
}

View File

@ -35,6 +35,13 @@ public sealed class NotificationService
private void LogAppUserModelRegistrationState()
{
if (!string.IsNullOrWhiteSpace(ToastShortcutRegistration.LastDiagnostics))
{
_logging.Warning(
$"Windows toast: ярлык Start Menu не подготовлен ({ToastShortcutRegistration.LastDiagnostics})",
"notify");
}
if (string.Equals(AppUserModelIdRegistration.LastRegisteredId, ToastAppUserModelId, StringComparison.Ordinal))
{
_logging.Info($"App User Model ID: {ToastAppUserModelId}", "notify");