diff --git a/Cargo.toml b/Cargo.toml index c71317fc..d213af1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,17 @@ windows = { version = "0.61", features = [ "Foundation", "Networking_PushNotifications", "UI_Notifications", + "Win32_Foundation", + "Win32_System_Com", + "Win32_UI_Notifications", ] } +# The `#[implement]` macro from `windows::core::implement` emits `::windows_core::*` +# absolute paths, so `windows-core` must be a direct dependency, not just +# transitive through `windows`. +windows-core = "0.61" +# Used to parse user-supplied CLSID strings (accepts braced, unbraced, and +# hyphen-less forms) before converting to `windows::core::GUID` via raw bits. +uuid = { version = "1", default-features = false } nt-time = "0.15" [build-dependencies] diff --git a/examples/notifications-demo/package.json b/examples/notifications-demo/package.json index 3753e889..220e7604 100644 --- a/examples/notifications-demo/package.json +++ b/examples/notifications-demo/package.json @@ -9,7 +9,8 @@ "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "tauri": "tauri" + "tauri": "tauri", + "tauri:windows:build": "tauri-windows-bundle build" }, "license": "MIT", "dependencies": { @@ -25,6 +26,7 @@ "svelte": "^5.56.1", "svelte-check": "^4.4.4", "typescript": "~6.0.3", - "vite": "^8.0.13" + "vite": "^8.0.13", + "@choochmeque/tauri-windows-bundle": "^0.1.20" } } diff --git a/examples/notifications-demo/pnpm-lock.yaml b/examples/notifications-demo/pnpm-lock.yaml index fea3e550..0fba3083 100644 --- a/examples/notifications-demo/pnpm-lock.yaml +++ b/examples/notifications-demo/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: specifier: ^2 version: 2.5.4 devDependencies: + '@choochmeque/tauri-windows-bundle': + specifier: ^0.1.20 + version: 0.1.21 '@sveltejs/adapter-static': specifier: ^3.0.6 version: 3.0.10(@sveltejs/kit@2.64.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.58.0))(vite@8.0.16(esbuild@0.27.2)))(svelte@5.56.3(@typescript-eslint/types@8.58.0))(typescript@6.0.3)(vite@8.0.16(esbuild@0.27.2))) @@ -48,6 +51,10 @@ packages: '@choochmeque/tauri-plugin-notifications-api@file:../..': resolution: {directory: ../.., type: directory} + '@choochmeque/tauri-windows-bundle@0.1.21': + resolution: {integrity: sha512-z5DuRUhokC64NvT3bFyEzPuxJiZ1xc3yUzF0S3HoUzhQpnivzjU13EIgEfC4f8USmu95V9sgBApOkl7GOIiYvA==} + hasBin: true + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -471,6 +478,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + aria-query@5.3.1: resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} engines: {node: '>= 0.4'} @@ -479,6 +490,23 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + binary-search@1.3.6: + resolution: {integrity: sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + bresenham-zingl@0.2.5: + resolution: {integrity: sha512-0TCrPvavRM/3Os9QIUDhe8jpAQXgEYjsVO+9IKnkC8cdzorDX5fMRfr3neEFGjcfRItEswHvIvPrqQzCalRYkw==} + + cheminfo-types@1.15.0: + resolution: {integrity: sha512-shv45WN2u0yN9EHH1bisNrv+fy4Cw+eLM5lOoriP67mePrwbHZ1kJqg90C8GEU7K1A8gJsicEoVZHcuBbuul/w==} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -487,10 +515,26 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -518,6 +562,15 @@ packages: '@typescript-eslint/types': optional: true + fast-bmp@4.0.1: + resolution: {integrity: sha512-+KtMijJj+uA8Sl6EXAnhza7US7EXSY5Z9NeiJwT1wopVUksyLMXL5iFmn9FjY8FdkstOkpJI9RuEVXkGpIPSwg==} + + fast-jpeg@3.0.1: + resolution: {integrity: sha512-1r4CO4rgRY1CeqaHlXVBPhdGC+rbNwWFJvV9s3HTUC+svqTE7hC1cEH9srp5MPgAmhDdsZ/WtU3WGM8zVobFCA==} + + fast-png@8.0.0: + resolution: {integrity: sha512-gCysNasJ8KEMgfdYIKd/wTDo6ENK1PWT0RJO7O+0pgmuHPw2O6tA1WvdxFRJoLf9V8yFYpG0FA1YgI8X97OhJA==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -527,14 +580,63 @@ packages: picomatch: optional: true + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + + fft.js@4.0.4: + resolution: {integrity: sha512-f9c00hphOgeQTlDyavwTtu6RiK8AIFjD6+jvXkNkpeQ7rirK3uFWVpalkoS4LAwbdX7mfZ8aoBfFVQX1Re/8aw==} + + file-type@10.11.0: + resolution: {integrity: sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==} + engines: {node: '>=6'} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + image-js@1.6.1: + resolution: {integrity: sha512-lLhyJCj7Ip5I0nJLYlsir1SBlDJACracpcMkZtRN+FnrS1eN3SHD0F2Yb7kmfVxCp3EV9EcMyCXFfKCjHLRTEw==} + + image-type@4.1.0: + resolution: {integrity: sha512-CFJMJ8QK8lJvRlTCEgarL4ro6hfDQKif2HjSvYCdQZESaIPV4v9imrf7BQHK+sQeTeNeMpWciR9hyC/g8ybXEg==} + engines: {node: '>=6'} + + iobuffer@6.0.1: + resolution: {integrity: sha512-SZWYkWNfjIXIBYSDpXDYIgshqtbOPsi4lviawAEceR1Kqk+sHDlcQjWrzNQsii80AyBY0q5c8HCTNjqo74ul+Q==} + + is-any-array@2.0.1: + resolution: {integrity: sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==} + + is-any-array@3.0.0: + resolution: {integrity: sha512-o4h+tylWykC4BD1vaejp6gDxoM13bwW8FGuNs4yIKpj8xbBJcRxJx8vZpq0dCr7ZDEfeKjmsi/euolKhX6f/ww==} + is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + + js-priority-queue@0.1.5: + resolution: {integrity: sha512-2dPmJT4GbXUpob7AZDR1wFMKz3Biy6oW69mwt5PTtdeoOgDin1i0p5gUV9k0LFeUxDpwkfr+JGMZDpcprjiY5w==} + kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -612,9 +714,69 @@ packages: locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + median-quickselect@1.0.1: + resolution: {integrity: sha512-/QL9ptNuLsdA68qO+2o10TKCyu621zwwTFdLvtu8rzRNKsn8zvuGoq/vDxECPyELFG8wu+BpyoMR9BnsJqfVZQ==} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ml-affine-transform@1.0.3: + resolution: {integrity: sha512-uv13li9HOGuMdSKL+QEdaV25emO9kt17LzGCD+FLQrBsrU8p/r/3kS/GJ7xI+f3/aWghkBK733x0yBG6EwKFJA==} + + ml-array-max@2.0.0: + resolution: {integrity: sha512-QQZ4kENwpWmyNb98UXRDFXrmtIXuXtt1+bSbda/2KA85+F+rrJP8hZk6QOkCQXM2Th9mUDYdq/PNByPdT9ID4A==} + + ml-array-median@1.1.6: + resolution: {integrity: sha512-V6bV6bTPFRX8v5CaAx/7fuRXC39LLTHfPSVZZafdNaqNz2PFL5zEA7gesjv8dMXh+gwPeUMtB5QPovlTBaa4sw==} + + ml-array-min@2.0.0: + resolution: {integrity: sha512-GRj6Ky6sW9vGL6yIjgsHmXZ9YgrdmcQ8nCxPqEGeKc6dkfYg1XDYxGFxADUjNuZyoCd5PUscWAS4N+cFaX6hFg==} + + ml-array-rescale@2.0.0: + resolution: {integrity: sha512-2GGtKfSno94/kIloWGvpp/U5Q5vLvLrza+SAaGsLeo6Xj4mEbA6Gqx+oTfZFkxnd1grT2X007HfJNs3T5BsiVg==} + + ml-convolution@2.0.0: + resolution: {integrity: sha512-ExW6zVmN2YGuyO3aBiS1ymybme3nVgv2ccCfynSdgtW5sNp4DOHnfow4K/ErTDMQ1s9tINjv7kvzjnVhQHXJBA==} + + ml-matrix@6.12.2: + resolution: {integrity: sha512-GC+BnW+pBh8Auap8goAxY0senAmF0IEoc3HNVSfnfbvGw0buuDIYb9kAKMS1l+GiwJ1rfK2bzJ8IHhwjzATSFA==} + + ml-random@0.5.0: + resolution: {integrity: sha512-zLJBmNb34LOz+vN6BD8l3aYm/VWYWbmAunrLMPs4dHf4gTl8BWlhil72j56HubPg86zrXioIs4qoHq7Topy6tw==} + + ml-ransac@1.0.0: + resolution: {integrity: sha512-fGi45s7T5+BEqdb5JqZC699w2riy6j0tzuZ2ZP63xavRa/yVKQIbUbj4XiE4XMr9T7HSiq1D9y0uybiyikDPvA==} + + ml-regression-base@4.0.1: + resolution: {integrity: sha512-kYJWRIfB88JDUN+QWBsOlI0dP5tMo/XPxqVU0Xjn6B0rfzPptUnON9xPNnMVEm43DkM621j9v6+Ln9vCk3N93A==} + + ml-regression-multivariate-linear@2.0.4: + resolution: {integrity: sha512-/vShPAlP+mB7P2mC5TuXwObSJNl/UBI71/bszt9ilTg6yLKy6btDLpAYyJNa6t+JnL5a7q+Yy4dCltfpvqXRIw==} + + ml-regression-polynomial-2d@1.0.0: + resolution: {integrity: sha512-r34kaawZKMwjORIdzpa/o5fnjlTXkkaLJ3B4vvFNeKlvEGPtApqFxbDRmw07e2CjUH2MhGIChpMeXp/XB3Iogg==} + + ml-spectra-processing@14.29.0: + resolution: {integrity: sha512-825CS864krbjMv7OB0mbjgAmyOL5ymj1OGa0gAzz1h1Dcd3Eeol2DaOimSiPYmRhW+iYhpeQnb7cSU0mlSK6+g==} + + ml-xsadd@2.0.0: + resolution: {integrity: sha512-VoAYUqmPRmzKbbqRejjqceGFp3VF81Qe8XXFGU0UXLxB7Mf4GGvyGq5Qn3k4AiQgDEV6WzobqlPOd+j0+m6IrA==} + + ml-xsadd@3.0.1: + resolution: {integrity: sha512-Fz2q6dwgzGM8wYKGArTUTZDGa4lQFA2Vi6orjGeTVRy22ZnQFKlJuwS9n8NRviqz1KHAHAzdKJwbnYhdo38uYg==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -623,14 +785,27 @@ packages: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + next-power-of-two@1.0.0: + resolution: {integrity: sha512-+z6QY1SxkDk6CQJAeaIZKmcNubBCRP7J8DMQUBglz/sSkNsZoJ1kULjqk9skNPPplzs4i9PFhYrvNDdtQleF/A==} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + parenthesis@3.1.8: + resolution: {integrity: sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -646,6 +821,21 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + robust-orientation@1.2.1: + resolution: {integrity: sha512-FuTptgKwY6iNuU15nrIJDLjXzCChWB+T4AvksRtwPS/WZ3HuP1CElCm1t+OBfgQKfWbtZIawip+61k7+buRKAg==} + + robust-point-in-polygon@1.0.3: + resolution: {integrity: sha512-pPzz7AevOOcPYnFv4Vs5L0C7BKOq6C/TfAw5EUE58CylbjGiPyMjAnPLzzSuPZ2zftUGwWbmLWPOjPOz61tAcA==} + + robust-scale@1.0.2: + resolution: {integrity: sha512-jBR91a/vomMAzazwpsPTPeuTPPmWBacwA+WYGNKcRGSh6xweuQ2ZbjRZ4v792/bZOhRKXRiQH0F48AvuajY0tQ==} + + robust-subtract@1.0.0: + resolution: {integrity: sha512-xhKUno+Rl+trmxAIVwjQMiVdpF5llxytozXJOdoT4eTIqmqsndQqFb1A0oiW3sZGlhMRhOi6pAD4MF1YYW6o/A==} + + robust-sum@1.0.0: + resolution: {integrity: sha512-AvLExwpaqUqD1uwLU6MwzzfRdaI6VEZsyvQ3IAQ0ZJ08v1H+DTyqskrf2ZJyh0BDduFVLN7H04Zmc+qTiahhAw==} + rolldown@1.0.3: resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -662,10 +852,19 @@ packages: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} + skia-canvas@3.0.8: + resolution: {integrity: sha512-FSYKxp8Ng2vOeeOBiyPhnn6ui6FirPJXMyjk4PKl8N/OWzVrkMawUgY9zubIWHMdYtyWFn0gfX3QlRwg6HBmdg==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + ssim.js@3.5.0: + resolution: {integrity: sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==} + + string-split-by@1.0.0: + resolution: {integrity: sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==} + svelte-check@4.6.0: resolution: {integrity: sha512-KhVnDFDSid57mmZtHz8gfW8AAGylOZ0vPnOIzVmAL+urzwK8sBYXRss953gD8T0OdgAQ11mdWhE6uadmtOz8TQ==} engines: {node: '>= 18.0.0'} @@ -678,6 +877,9 @@ packages: resolution: {integrity: sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA==} engines: {node: '>=18'} + tiff@7.1.3: + resolution: {integrity: sha512-YEEq3fT++2pdta/9P/vGG4QRMdZQoe6W6JNaWnIi6NvAsbeNITwFCtmWwL/BZvOi+uo2I3ohyOkD3sZfme+c6g==} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -686,14 +888,26 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + ts-pattern@5.9.0: + resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + two-product@1.0.2: + resolution: {integrity: sha512-vOyrqmeYvzjToVM08iU52OFocWT6eB/I5LUWYnxeAPGXAhAxXYU/Yr/R2uY5/5n4bvJQL9AQulIuxpIsMoT8XQ==} + + two-sum@1.0.0: + resolution: {integrity: sha512-phP48e8AawgsNUjEY2WvoIWqdie8PoiDZGxTDv70LDr01uX5wLEQbOgSP7Z/B6+SW5oLtbe8qaYX2fKJs3CGTw==} + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true + uint8-base64@1.0.0: + resolution: {integrity: sha512-B6ZJfEZL2FAhbIyZZ7WlQq8MPq1X1P1DUA//LsSv/vB4r+Dao5/BPoxgw2t+t1XyvoSfBkE7zI4SYsnGj7BmlQ==} + vite@8.0.16: resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -754,6 +968,15 @@ snapshots: dependencies: '@tauri-apps/api': 2.11.0 + '@choochmeque/tauri-windows-bundle@0.1.21': + dependencies: + commander: 15.0.0 + glob: 13.0.6 + image-js: 1.6.1 + transitivePeerDependencies: + - debug + - supports-color + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -1039,18 +1262,42 @@ snapshots: acorn@8.16.0: {} + agent-base@7.1.4: + optional: true + aria-query@5.3.1: {} axobject-query@4.1.0: {} + balanced-match@4.0.4: {} + + binary-search@1.3.6: {} + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + bresenham-zingl@0.2.5: {} + + cheminfo-types@1.15.0: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 clsx@2.1.1: {} + colord@2.9.3: {} + + commander@15.0.0: {} + cookie@0.6.0: {} + debug@4.4.3: + dependencies: + ms: 2.1.3 + optional: true + deepmerge@4.3.1: {} detect-libc@2.1.2: {} @@ -1095,17 +1342,97 @@ snapshots: optionalDependencies: '@typescript-eslint/types': 8.58.0 + fast-bmp@4.0.1: + dependencies: + iobuffer: 6.0.1 + + fast-jpeg@3.0.1: + dependencies: + iobuffer: 6.0.1 + tiff: 7.1.3 + + fast-png@8.0.0: + dependencies: + fflate: 0.8.3 + iobuffer: 6.0.1 + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 + fflate@0.8.3: {} + + fft.js@4.0.4: {} + + file-type@10.11.0: {} + + follow-redirects@1.16.0: + optional: true + fsevents@2.3.3: optional: true + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + image-js@1.6.1: + dependencies: + bresenham-zingl: 0.2.5 + colord: 2.9.3 + fast-bmp: 4.0.1 + fast-jpeg: 3.0.1 + fast-png: 8.0.0 + image-type: 4.1.0 + jpeg-js: 0.4.4 + js-priority-queue: 0.1.5 + median-quickselect: 1.0.1 + ml-affine-transform: 1.0.3 + ml-convolution: 2.0.0 + ml-matrix: 6.12.2 + ml-ransac: 1.0.0 + ml-regression-multivariate-linear: 2.0.4 + ml-regression-polynomial-2d: 1.0.0 + ml-spectra-processing: 14.29.0 + robust-point-in-polygon: 1.0.3 + ssim.js: 3.5.0 + tiff: 7.1.3 + ts-pattern: 5.9.0 + uint8-base64: 1.0.0 + optionalDependencies: + skia-canvas: 3.0.8 + transitivePeerDependencies: + - debug + - supports-color + + image-type@4.1.0: + dependencies: + file-type: 10.11.0 + + iobuffer@6.0.1: {} + + is-any-array@2.0.1: {} + + is-any-array@3.0.0: {} + is-reference@3.0.3: dependencies: '@types/estree': 1.0.9 + jpeg-js@0.4.4: {} + + js-priority-queue@0.1.5: {} + kleur@4.1.5: {} lightningcss-android-arm64@1.32.0: @@ -1159,18 +1486,113 @@ snapshots: locate-character@3.0.0: {} + lru-cache@11.5.1: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + median-quickselect@1.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minipass@7.1.3: {} + + ml-affine-transform@1.0.3: + dependencies: + ml-matrix: 6.12.2 + + ml-array-max@2.0.0: + dependencies: + is-any-array: 3.0.0 + + ml-array-median@1.1.6: + dependencies: + is-any-array: 2.0.1 + median-quickselect: 1.0.1 + + ml-array-min@2.0.0: + dependencies: + is-any-array: 3.0.0 + + ml-array-rescale@2.0.0: + dependencies: + is-any-array: 3.0.0 + ml-array-max: 2.0.0 + ml-array-min: 2.0.0 + + ml-convolution@2.0.0: + dependencies: + fft.js: 4.0.4 + next-power-of-two: 1.0.0 + + ml-matrix@6.12.2: + dependencies: + is-any-array: 3.0.0 + ml-array-rescale: 2.0.0 + + ml-random@0.5.0: + dependencies: + ml-xsadd: 2.0.0 + + ml-ransac@1.0.0: + dependencies: + ml-array-median: 1.1.6 + ml-matrix: 6.12.2 + ml-random: 0.5.0 + + ml-regression-base@4.0.1: + dependencies: + cheminfo-types: 1.15.0 + is-any-array: 3.0.0 + + ml-regression-multivariate-linear@2.0.4: + dependencies: + ml-matrix: 6.12.2 + + ml-regression-polynomial-2d@1.0.0: + dependencies: + cheminfo-types: 1.15.0 + is-any-array: 2.0.1 + ml-matrix: 6.12.2 + ml-regression-base: 4.0.1 + + ml-spectra-processing@14.29.0: + dependencies: + binary-search: 1.3.6 + cheminfo-types: 1.15.0 + fft.js: 4.0.4 + is-any-array: 3.0.0 + ml-matrix: 6.12.2 + ml-xsadd: 3.0.1 + + ml-xsadd@2.0.0: {} + + ml-xsadd@3.0.1: {} + mri@1.2.0: {} mrmime@2.0.1: {} + ms@2.1.3: + optional: true + nanoid@3.3.12: {} + next-power-of-two@1.0.0: {} + obug@2.1.1: {} + parenthesis@3.1.8: + optional: true + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.1 + minipass: 7.1.3 + picocolors@1.1.1: {} picomatch@4.0.4: {} @@ -1183,6 +1605,26 @@ snapshots: readdirp@4.1.2: {} + robust-orientation@1.2.1: + dependencies: + robust-scale: 1.0.2 + robust-subtract: 1.0.0 + robust-sum: 1.0.0 + two-product: 1.0.2 + + robust-point-in-polygon@1.0.3: + dependencies: + robust-orientation: 1.2.1 + + robust-scale@1.0.2: + dependencies: + two-product: 1.0.2 + two-sum: 1.0.0 + + robust-subtract@1.0.0: {} + + robust-sum@1.0.0: {} + rolldown@1.0.3: dependencies: '@oxc-project/types': 0.133.0 @@ -1216,8 +1658,26 @@ snapshots: mrmime: 2.0.1 totalist: 3.0.1 + skia-canvas@3.0.8: + dependencies: + detect-libc: 2.1.2 + follow-redirects: 1.16.0 + https-proxy-agent: 7.0.6 + string-split-by: 1.0.0 + transitivePeerDependencies: + - debug + - supports-color + optional: true + source-map-js@1.2.1: {} + ssim.js@3.5.0: {} + + string-split-by@1.0.0: + dependencies: + parenthesis: 3.1.8 + optional: true + svelte-check@4.6.0(picomatch@4.0.4)(svelte@5.56.3(@typescript-eslint/types@8.58.0))(typescript@6.0.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -1252,6 +1712,11 @@ snapshots: transitivePeerDependencies: - '@typescript-eslint/types' + tiff@7.1.3: + dependencies: + fflate: 0.8.3 + iobuffer: 6.0.1 + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -1259,11 +1724,19 @@ snapshots: totalist@3.0.1: {} + ts-pattern@5.9.0: {} + tslib@2.8.1: optional: true + two-product@1.0.2: {} + + two-sum@1.0.0: {} + typescript@6.0.3: {} + uint8-base64@1.0.0: {} + vite@8.0.16(esbuild@0.27.2): dependencies: lightningcss: 1.32.0 diff --git a/examples/notifications-demo/src-tauri/gen/windows/.gitignore b/examples/notifications-demo/src-tauri/gen/windows/.gitignore new file mode 100644 index 00000000..64e107ed --- /dev/null +++ b/examples/notifications-demo/src-tauri/gen/windows/.gitignore @@ -0,0 +1,2 @@ +# Generated files +# Keep bundle.config.json and templates in git diff --git a/examples/notifications-demo/src-tauri/gen/windows/AppxManifest.xml.template b/examples/notifications-demo/src-tauri/gen/windows/AppxManifest.xml.template new file mode 100644 index 00000000..eff062fc --- /dev/null +++ b/examples/notifications-demo/src-tauri/gen/windows/AppxManifest.xml.template @@ -0,0 +1,51 @@ + + + + + + + {{DISPLAY_NAME}} + {{PUBLISHER_DISPLAY_NAME}} + Assets\StoreLogo.png + + + + + + + + + + + + + + + + +{{EXTENSIONS}} + + + + +{{CAPABILITIES}} + + diff --git a/examples/notifications-demo/src-tauri/gen/windows/Assets/LargeTile.png b/examples/notifications-demo/src-tauri/gen/windows/Assets/LargeTile.png new file mode 100644 index 00000000..f9bc0483 Binary files /dev/null and b/examples/notifications-demo/src-tauri/gen/windows/Assets/LargeTile.png differ diff --git a/examples/notifications-demo/src-tauri/gen/windows/Assets/Square150x150Logo.png b/examples/notifications-demo/src-tauri/gen/windows/Assets/Square150x150Logo.png new file mode 100644 index 00000000..624c7bfb Binary files /dev/null and b/examples/notifications-demo/src-tauri/gen/windows/Assets/Square150x150Logo.png differ diff --git a/examples/notifications-demo/src-tauri/gen/windows/Assets/Square44x44Logo.png b/examples/notifications-demo/src-tauri/gen/windows/Assets/Square44x44Logo.png new file mode 100644 index 00000000..d5fbfb2a Binary files /dev/null and b/examples/notifications-demo/src-tauri/gen/windows/Assets/Square44x44Logo.png differ diff --git a/examples/notifications-demo/src-tauri/gen/windows/Assets/StoreLogo.png b/examples/notifications-demo/src-tauri/gen/windows/Assets/StoreLogo.png new file mode 100644 index 00000000..45563882 Binary files /dev/null and b/examples/notifications-demo/src-tauri/gen/windows/Assets/StoreLogo.png differ diff --git a/examples/notifications-demo/src-tauri/gen/windows/Assets/Wide310x150Logo.png b/examples/notifications-demo/src-tauri/gen/windows/Assets/Wide310x150Logo.png new file mode 100644 index 00000000..79d394be Binary files /dev/null and b/examples/notifications-demo/src-tauri/gen/windows/Assets/Wide310x150Logo.png differ diff --git a/examples/notifications-demo/src-tauri/gen/windows/bundle.config.json b/examples/notifications-demo/src-tauri/gen/windows/bundle.config.json new file mode 100644 index 00000000..63f6cdc3 --- /dev/null +++ b/examples/notifications-demo/src-tauri/gen/windows/bundle.config.json @@ -0,0 +1,26 @@ +{ + "publisher": "CN=YourCompany", + "publisherDisplayName": "Your Company Name", + "capabilities": { + "general": [ + "internetClient" + ] + }, + "extensions": { + "shareTarget": false, + "fileAssociations": [], + "protocolHandlers": [], + "toastActivation": { + "activationType": "foreground", + "clsid": "{757B33CB-099E-4AA9-8741-314D2B54B59D}" + } + }, + "signing": { + "pfx": null, + "pfxPassword": null + }, + "resourceIndex": { + "enabled": false, + "keepConfig": false + } +} diff --git a/examples/notifications-demo/src-tauri/tauri.conf.json b/examples/notifications-demo/src-tauri/tauri.conf.json index 30f9ee88..33b4d97d 100644 --- a/examples/notifications-demo/src-tauri/tauri.conf.json +++ b/examples/notifications-demo/src-tauri/tauri.conf.json @@ -21,6 +21,13 @@ "csp": null } }, + "plugins": { + "notifications": { + "windows": { + "toastActivatorClsid": "{757B33CB-099E-4AA9-8741-314D2B54B59D}" + } + } + }, "bundle": { "active": true, "targets": "all", diff --git a/src/lib.rs b/src/lib.rs index 52f1ad98..9bcebd1b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ //! Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API. -use serde::Serialize; +use serde::{Deserialize, Serialize}; #[cfg(mobile)] use tauri::plugin::PluginHandle; #[cfg(desktop)] @@ -10,6 +10,33 @@ use tauri::{ Manager, Runtime, }; +/// Top-level plugin config deserialized from the `plugins.notifications` block +/// in `tauri.conf.json`. Optional in its entirety — apps without a config block +/// get `PluginConfig::default()`. +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct PluginConfig { + #[cfg(target_os = "windows")] + pub windows: WindowsConfig, +} + +/// Windows-only plugin config. +/// +/// Currently carries the toast activator CLSID used by +/// `INotificationActivationCallback` registration; absent value means +/// COM-based activation is disabled and the plugin falls back to in-process +/// `Activated` events only. +#[cfg(target_os = "windows")] +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct WindowsConfig { + /// Toast activator CLSID. Must match the GUID declared in the MSIX + /// manifest's `` + /// and `` entries. Accepts the `xxxxxxxx-xxxx-...` form + /// with or without surrounding braces. + pub toast_activator_clsid: Option, +} + pub use models::*; pub use tauri::plugin::PermissionState; @@ -266,8 +293,8 @@ impl> crate::NotificationsExt for T { /// Initializes the plugin. #[must_use] -pub fn init() -> TauriPlugin { - Builder::new("notifications") +pub fn init() -> TauriPlugin { + Builder::::new("notifications") .invoke_handler(tauri::generate_handler![ commands::notify, commands::request_permission, @@ -299,6 +326,8 @@ pub fn init() -> TauriPlugin { .setup(|app, api| { #[cfg(desktop)] listeners::init(); + #[cfg(all(target_os = "windows", not(feature = "notify-rust")))] + let windows_config = api.config().windows.clone(); #[cfg(mobile)] let notification = mobile::init(app, api)?; #[cfg(all(desktop, any(feature = "notify-rust", target_os = "linux")))] @@ -306,7 +335,7 @@ pub fn init() -> TauriPlugin { #[cfg(all(target_os = "macos", not(feature = "notify-rust")))] let notification = macos::init(app, api)?; #[cfg(all(target_os = "windows", not(feature = "notify-rust")))] - let notification = windows::init(app, api)?; + let notification = windows::init(app, api, windows_config)?; app.manage(notification); Ok(()) }) diff --git a/src/listeners.rs b/src/listeners.rs index a2532236..9df483e5 100644 --- a/src/listeners.rs +++ b/src/listeners.rs @@ -10,6 +10,10 @@ use std::collections::HashMap; use std::sync::{OnceLock, RwLock}; +#[cfg(all(target_os = "windows", not(feature = "notify-rust")))] +use tauri::Manager; +use tauri::{AppHandle, Runtime}; + use crate::error::{ErrorResponse, PluginInvokeError}; type ChannelMap = HashMap>; @@ -66,14 +70,21 @@ pub fn trigger(event: &str, payload: String) -> crate::Result<()> { } /// Register a channel to receive events for the given event name. +/// +/// On Windows, subscribing to `notificationClicked` synchronously drains any +/// cold-start activation payload buffered by the COM activator before the JS +/// listener was up. That makes the `push-listener.tsx` contract ("subscribing +/// flushes the buffered tap") work without the app calling any extra command. // Tauri commands receive serde-deserialized owned values. #[allow(clippy::needless_pass_by_value)] #[tauri::command] -pub fn register_listener( +pub fn register_listener( + app: AppHandle, event: String, handler: tauri::ipc::Channel, ) -> crate::Result<()> { let listeners = LISTENERS.get_or_init(|| RwLock::new(HashMap::new())); + let should_drain_clicks = event == "notificationClicked"; { let mut guard = listeners.write().map_err(|e| { crate::Error::from(PluginInvokeError::InvokeRejected(ErrorResponse { @@ -87,6 +98,14 @@ pub fn register_listener( .or_default() .insert(handler.id(), handler); } + #[cfg(all(target_os = "windows", not(feature = "notify-rust")))] + if should_drain_clicks { + if let Some(notif) = app.try_state::>() { + notif.drain_pending_clicks(); + } + } + #[cfg(not(all(target_os = "windows", not(feature = "notify-rust"))))] + let _ = (app, should_drain_clicks); Ok(()) } diff --git a/src/windows.rs b/src/windows.rs index d3a599a2..9347811e 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -1,7 +1,8 @@ //! Windows implementation for notifications plugin using native Windows Toast API. use std::collections::HashMap; -use std::sync::{Arc, RwLock}; +use std::ffi::c_void; +use std::sync::{Arc, RwLock, Weak}; use nt_time::FileTime; use serde::de::DeserializeOwned; @@ -9,7 +10,7 @@ use tauri::{ plugin::{PermissionState, PluginApi}, AppHandle, Runtime, }; -use windows::core::{Interface, HSTRING}; +use windows::core::{implement, Interface, Ref, BOOL, GUID, HSTRING, PCWSTR}; use windows::ApplicationModel::Package; use windows::Data::Xml::Dom::XmlDocument; use windows::Foundation::{DateTime, TypedEventHandler}; @@ -17,11 +18,24 @@ use windows::Foundation::{DateTime, TypedEventHandler}; use windows::Networking::PushNotifications::{ PushNotificationChannel, PushNotificationChannelManager, }; +use windows::Win32::Foundation::{CLASS_E_NOAGGREGATION, E_INVALIDARG, S_FALSE, S_OK}; +use windows::Win32::System::Com::{ + CoInitializeEx, CoRegisterClassObject, IClassFactory, IClassFactory_Impl, CLSCTX_LOCAL_SERVER, + COINIT_APARTMENTTHREADED, REGCLS_MULTIPLEUSE, +}; +use windows::Win32::UI::Notifications::{ + INotificationActivationCallback, INotificationActivationCallback_Impl, + NOTIFICATION_USER_INPUT_DATA, +}; use windows::UI::Notifications::{ NotificationSetting, ScheduledToastNotification, ToastActivatedEventArgs, ToastNotification, ToastNotificationManager, ToastNotifier, }; +use crate::error::{ErrorResponse, PluginInvokeError}; +use crate::models::*; +use crate::WindowsConfig; + /// True when the current process has MSIX package identity. /// /// WinRT notification APIs split into two flavors: no-arg variants that use the @@ -35,8 +49,17 @@ fn is_packaged() -> bool { Package::Current().is_ok() } -use crate::error::{ErrorResponse, PluginInvokeError}; -use crate::models::*; +/// Accept any well-formed UUID string and reinterpret its bytes as a `GUID`. +/// +/// Delegating to `uuid::Uuid::parse_str` lets the manifest CLSID and the +/// `tauri.conf.json` CLSID use either braced (`{xxxxxxxx-…}`), unbraced +/// (`xxxxxxxx-…`), or simple (32 hex chars, no hyphens) conventions without +/// drift causing parse failures. +fn parse_clsid(raw: &str) -> windows::core::Result { + let parsed = uuid::Uuid::parse_str(raw.trim()) + .map_err(|e| windows::core::Error::new(E_INVALIDARG, format!("{e}")))?; + Ok(GUID::from_u128(parsed.as_u128())) +} // Enable `?` operator for windows::core::Error impl From for crate::Error { @@ -56,10 +79,41 @@ pub struct WindowsPlugin { notifier: ToastNotifier, action_types: RwLock>, click_listener_active: RwLock, + /// Cold-start activation payloads queued before any JS listener has + /// subscribed. Drained synchronously the first time a `notificationClicked` + /// listener registers (see `crate::listeners::register_listener`). + pending_clicks: RwLock>, + /// `CoRegisterClassObject` cookie. Kept for the process lifetime — no + /// explicit `CoRevokeClassObject` on shutdown; the OS reclaims it on exit. + /// `None` when COM activator wasn't registered (unpackaged or no CLSID in + /// config). + _com_cookie: RwLock>, #[cfg(feature = "push-notifications")] push_channel: RwLock>, } +/// COM activator that receives toast activations from Action Center, including +/// the cold-start case where Windows launches the exe via the manifest's +/// `windows.toastNotificationActivation` extension. +/// +/// Wired up by `init()` only when the process has MSIX package identity AND +/// the plugin config carries a valid `toast_activator_clsid`. The callback +/// fires on a COM RPC worker thread (not the Tauri main thread), so all +/// downstream emissions must be thread-safe — `crate::listeners::trigger` +/// already is. +#[implement(INotificationActivationCallback)] +struct ToastActivator { + plugin: Weak, +} + +/// Out-of-proc COM activator pattern requires a class factory; `CoRegisterClassObject` +/// takes an `IUnknown` that must implement `IClassFactory`, not the activator +/// instance directly. There is no shortcut for the toast activator path. +#[implement(IClassFactory)] +struct ToastActivatorFactory { + plugin: Weak, +} + impl std::fmt::Debug for WindowsPlugin { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("WindowsPlugin") @@ -69,6 +123,91 @@ impl std::fmt::Debug for WindowsPlugin { } } +/// Build the activation payload JSON consumed by both the in-process +/// `ToastNotification.Activated` handler and the COM activator. Kept here so +/// warm and cold paths emit byte-identical event shapes — the JS side can't +/// distinguish them. +fn activation_payload(invoked_args: &str, inputs: &HashMap) -> serde_json::Value { + let action_id = if invoked_args.is_empty() { + "tap" + } else { + invoked_args + }; + let input_value = inputs + .values() + .next() + .cloned() + .map_or(serde_json::Value::Null, serde_json::Value::String); + serde_json::json!({ + "actionId": action_id, + "inputValue": input_value, + "notification": serde_json::Value::Null, + }) +} + +impl INotificationActivationCallback_Impl for ToastActivator_Impl { + fn Activate( + &self, + _appusermodelid: &PCWSTR, + invokedargs: &PCWSTR, + data: *const NOTIFICATION_USER_INPUT_DATA, + count: u32, + ) -> windows::core::Result<()> { + let invoked = unsafe { invokedargs.to_string() }.unwrap_or_default(); + let mut inputs: HashMap = HashMap::new(); + if count > 0 && !data.is_null() { + let slice = unsafe { std::slice::from_raw_parts(data, count as usize) }; + for entry in slice { + let k = unsafe { entry.Key.to_string() }.unwrap_or_default(); + let v = unsafe { entry.Value.to_string() }.unwrap_or_default(); + if !k.is_empty() { + inputs.insert(k, v); + } + } + } + + let action_payload = activation_payload(&invoked, &inputs); + let _ = crate::listeners::trigger("actionPerformed", action_payload.to_string()); + + if invoked.is_empty() { + let click_payload = serde_json::json!({ "id": serde_json::Value::Null, "data": {} }); + // Best-effort live emit; if no listeners are subscribed yet (cold + // start), the payload also goes onto the buffer below for drain + // when the JS side mounts. + let _ = crate::listeners::trigger("notificationClicked", click_payload.to_string()); + + if let Some(plugin) = self.plugin.upgrade() { + if let Ok(mut buf) = plugin.pending_clicks.write() { + buf.push(click_payload); + } + } + } + Ok(()) + } +} + +impl IClassFactory_Impl for ToastActivatorFactory_Impl { + fn CreateInstance( + &self, + punkouter: Ref<'_, windows::core::IUnknown>, + riid: *const GUID, + ppvobject: *mut *mut c_void, + ) -> windows::core::Result<()> { + if !punkouter.is_null() { + return Err(CLASS_E_NOAGGREGATION.into()); + } + let activator = ToastActivator { + plugin: self.plugin.clone(), + }; + let interface: INotificationActivationCallback = activator.into(); + unsafe { interface.query(riid, ppvobject).ok() } + } + + fn LockServer(&self, _flock: BOOL) -> windows::core::Result<()> { + Ok(()) + } +} + impl WindowsPlugin { fn action_types(&self) -> crate::Result> { Ok(self @@ -101,6 +240,25 @@ impl WindowsPlugin { Ok(()) } + /// Drain queued cold-start click payloads through the listener bus. Called + /// when a `notificationClicked` listener subscribes (see + /// `crate::listeners::register_listener`). Idempotent: subsequent calls + /// with an empty buffer are a no-op. + pub fn drain_pending_clicks(&self) { + let drained: Vec = match self.pending_clicks.write() { + Ok(mut buf) => std::mem::take(&mut *buf), + Err(e) => { + log::error!("pending_clicks lock poisoned during drain: {e}"); + return; + } + }; + for payload in drained { + if let Err(e) = crate::listeners::trigger("notificationClicked", payload.to_string()) { + log::error!("Failed to dispatch buffered click: {e}"); + } + } + } + fn open_push_channel(&self) -> crate::Result { #[cfg(feature = "push-notifications")] { @@ -148,6 +306,7 @@ impl WindowsPlugin { pub fn init( app: &AppHandle, _api: PluginApi, + windows_config: WindowsConfig, ) -> crate::Result> { let app_id = app.config().identifier.clone(); let packaged = is_packaged(); @@ -163,16 +322,68 @@ pub fn init( notifier, action_types: RwLock::new(HashMap::new()), click_listener_active: RwLock::new(false), + pending_clicks: RwLock::new(Vec::new()), + _com_cookie: RwLock::new(None), #[cfg(feature = "push-notifications")] push_channel: RwLock::new(None), }); + if packaged { + if let Some(clsid_str) = windows_config.toast_activator_clsid.as_deref() { + match register_toast_activator(&plugin, clsid_str) { + Ok(cookie) => { + if let Ok(mut slot) = plugin._com_cookie.write() { + *slot = Some(cookie); + } + log::info!("Toast activator registered (clsid={clsid_str}, cookie={cookie})"); + } + Err(e) => { + log::error!( + "Failed to register toast activator (clsid={clsid_str}): {e}; \ + Action Center clicks will fall back to shortcut launch without payload" + ); + } + } + } + } + Ok(Notifications { app: app.clone(), plugin, }) } +/// Initialize COM for the current thread (apartment-threaded) and register a +/// `ToastActivatorFactory` for the given CLSID. Returns the +/// `CoRegisterClassObject` cookie on success. +/// +/// `CoInitializeEx` is idempotent for the same apartment model: `S_OK` on +/// first call, `S_FALSE` on subsequent calls. `RPC_E_CHANGED_MODE` means +/// another component already initialized this thread as MTA — surfaced as an +/// error so the caller logs and skips registration. +fn register_toast_activator( + plugin: &Arc, + clsid_str: &str, +) -> windows::core::Result { + let clsid = parse_clsid(clsid_str)?; + let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }; + if hr != S_OK && hr != S_FALSE { + return Err(hr.into()); + } + let factory = ToastActivatorFactory { + plugin: Arc::downgrade(plugin), + }; + let factory_interface: IClassFactory = factory.into(); + unsafe { + CoRegisterClassObject( + &clsid, + &factory_interface, + CLSCTX_LOCAL_SERVER, + REGCLS_MULTIPLEUSE, + ) + } +} + impl crate::NotificationsBuilder { /// Build toast notification XML using DOM API (safer than string concatenation). fn build_toast_xml( @@ -455,6 +666,15 @@ impl Notifications { crate::NotificationsBuilder::new(self.app.clone(), self.plugin.clone()) } + /// Drain any cold-start activation payloads queued before the JS + /// `notificationClicked` listener subscribed. Invoked by + /// `crate::listeners::register_listener` on first subscription so the + /// `push-listener.tsx` contract ("subscribing flushes the buffered tap") + /// holds without the app having to call any extra command. + pub fn drain_pending_clicks(&self) { + self.plugin.drain_pending_clicks(); + } + pub async fn request_permission(&self) -> crate::Result { // Windows doesn't have a runtime permission prompt like mobile // We can only check the current state