diff --git a/resources/electron/electron-plugin/dist/server/api/notification.js b/resources/electron/electron-plugin/dist/server/api/notification.js index aba8f4c..ae7459a 100644 --- a/resources/electron/electron-plugin/dist/server/api/notification.js +++ b/resources/electron/electron-plugin/dist/server/api/notification.js @@ -1,26 +1,49 @@ import express from 'express'; import { Notification } from 'electron'; -import { notifyLaravel } from "../utils.js"; +import { notifyLaravel, broadcastToWindows } from "../utils.js"; +import playSoundLib from 'play-sound'; +import fs from 'fs'; +const isLocalFile = (sound) => { + if (typeof sound !== 'string') + return false; + if (/^https?:\/\//i.test(sound)) + return false; + return sound.includes('/') || sound.includes('\\'); +}; const router = express.Router(); router.post('/', (req, res) => { const { title, body, subtitle, silent, icon, hasReply, timeoutType, replyPlaceholder, sound, urgency, actions, closeButtonText, toastXml, event: customEvent, reference, } = req.body; const eventName = customEvent !== null && customEvent !== void 0 ? customEvent : '\\Native\\Desktop\\Events\\Notifications\\NotificationClicked'; const notificationReference = reference !== null && reference !== void 0 ? reference : (Date.now() + '.' + Math.random().toString(36).slice(2, 9)); + const usingLocalFile = isLocalFile(sound); const notification = new Notification({ title, body, subtitle, - silent, + silent: usingLocalFile ? true : silent, icon, hasReply, timeoutType, replyPlaceholder, - sound, + sound: usingLocalFile ? undefined : sound, urgency, actions, closeButtonText, toastXml }); + if (usingLocalFile && !silent) { + fs.access(sound, fs.constants.F_OK, (err) => { + if (err) { + broadcastToWindows('log', { + level: 'error', + message: `Sound file not found: ${sound}`, + context: { sound } + }); + return; + } + playSoundLib().play(sound, () => { }); + }); + } notification.on("click", (event) => { notifyLaravel('events', { event: eventName || '\\Native\\Desktop\\Events\\Notifications\\NotificationClicked', diff --git a/resources/electron/electron-plugin/src/server/api/notification.ts b/resources/electron/electron-plugin/src/server/api/notification.ts index 4c56ca8..0fc69f3 100644 --- a/resources/electron/electron-plugin/src/server/api/notification.ts +++ b/resources/electron/electron-plugin/src/server/api/notification.ts @@ -1,6 +1,16 @@ import express from 'express'; import { Notification } from 'electron'; -import {notifyLaravel} from "../utils.js"; +import {notifyLaravel, broadcastToWindows} from "../utils.js"; +declare const require: any; +import playSoundLib from 'play-sound'; +import fs from 'fs'; + +const isLocalFile = (sound: unknown) => { + if (typeof sound !== 'string') return false; + if (/^https?:\/\//i.test(sound)) return false; + // Treat any string containing path separators as a local file + return sound.includes('/') || sound.includes('\\'); +}; const router = express.Router(); router.post('/', (req, res) => { @@ -26,22 +36,39 @@ router.post('/', (req, res) => { const notificationReference = reference ?? (Date.now() + '.' + Math.random().toString(36).slice(2, 9)); + const usingLocalFile = isLocalFile(sound); + const notification = new Notification({ title, body, subtitle, - silent, + silent: usingLocalFile ? true : silent, icon, hasReply, timeoutType, replyPlaceholder, - sound, + sound: usingLocalFile ? undefined : sound, urgency, actions, closeButtonText, toastXml }); + if (usingLocalFile && !silent) { + fs.access(sound, fs.constants.F_OK, (err) => { + if (err) { + broadcastToWindows('log', { + level: 'error', + message: `Sound file not found: ${sound}`, + context: { sound } + }); + return; + } + + playSoundLib().play(sound, () => {}); + }); + } + notification.on("click", (event) => { notifyLaravel('events', { event: eventName || '\\Native\\Desktop\\Events\\Notifications\\NotificationClicked', diff --git a/resources/electron/package-lock.json b/resources/electron/package-lock.json index 7798334..58ea092 100644 --- a/resources/electron/package-lock.json +++ b/resources/electron/package-lock.json @@ -23,6 +23,7 @@ "get-port": "^7.1.0", "kill-sync": "^1.0.3", "nodemon": "^3.1.9", + "play-sound": "^1.1.3", "ps-node": "^0.1.6", "tree-kill": "^1.2.2", "yauzl": "^3.2.0" @@ -7960,6 +7961,15 @@ "node": ">= 0.8" } }, + "node_modules/find-exec": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/find-exec/-/find-exec-1.0.3.tgz", + "integrity": "sha512-gnG38zW90mS8hm5smNcrBnakPEt+cGJoiMkJwCU0IYnEb0H2NQk0NIljhNW+48oniCriFek/PH6QXbwsJo/qug==", + "license": "MIT", + "dependencies": { + "shell-quote": "^1.8.1" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -10861,6 +10871,15 @@ "node": ">=6" } }, + "node_modules/play-sound": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/play-sound/-/play-sound-1.1.6.tgz", + "integrity": "sha512-09eO4QiXNFXJffJaOW5P6x6F5RLihpLUkXttvUZeWml0fU6x6Zp7AjG9zaeMpgH2ZNvq4GR1ytB22ddYcqJIZA==", + "license": "MIT", + "dependencies": { + "find-exec": "1.0.3" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -11950,6 +11969,18 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", diff --git a/resources/electron/package.json b/resources/electron/package.json index e12efbd..7d16b78 100644 --- a/resources/electron/package.json +++ b/resources/electron/package.json @@ -52,6 +52,7 @@ "get-port": "^7.1.0", "kill-sync": "^1.0.3", "nodemon": "^3.1.9", + "play-sound": "^1.1.3", "ps-node": "^0.1.6", "tree-kill": "^1.2.2", "yauzl": "^3.2.0" diff --git a/src/Facades/Notification.php b/src/Facades/Notification.php index d6e81b2..f8a4f2b 100644 --- a/src/Facades/Notification.php +++ b/src/Facades/Notification.php @@ -7,6 +7,8 @@ /** * @method static static title(string $title) * @method static static event(string $event) + * @method static static sound(string $sound) + * @method static static silent(bool $silent = true) * @method static static message(string $body) * @method static static reference(string $reference) * @method static static hasReply(string $placeholder = '') diff --git a/src/Notification.php b/src/Notification.php index f9a95a6..ab13c6e 100644 --- a/src/Notification.php +++ b/src/Notification.php @@ -17,6 +17,10 @@ class Notification protected string $event = ''; + protected string $sound = ''; + + protected bool $silent = false; + private bool $hasReply = false; private string $replyPlaceholder = ''; @@ -54,6 +58,20 @@ public function event(string $event): self return $this; } + public function sound(string $sound): self + { + $this->sound = $sound; + + return $this; + } + + public function silent(bool $silent = true): self + { + $this->silent = $silent; + + return $this; + } + public function hasReply(string $placeholder = ''): self { $this->hasReply = true; @@ -83,6 +101,8 @@ public function show(): self 'title' => $this->title, 'body' => $this->body, 'event' => $this->event, + 'sound' => $this->sound, + 'silent' => $this->silent, 'hasReply' => $this->hasReply, 'replyPlaceholder' => $this->replyPlaceholder, 'actions' => array_map(fn (string $label) => [