From 879ffc066fa3a33fdeb7138aadcb808f2c1a9f71 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:30:20 +0000 Subject: [PATCH] tauri notifications (#5875) --- frontend/package-lock.json | 10 +++ frontend/package.json | 1 + .../public/locales/en-GB/translation.toml | 2 + frontend/src-tauri/Cargo.lock | 69 ++++++++++++++++- frontend/src-tauri/Cargo.toml | 1 + frontend/src-tauri/capabilities/default.json | 3 + frontend/src-tauri/src/lib.rs | 1 + .../hooks/tools/shared/useToolOperation.ts | 4 + .../services/desktopNotificationService.ts | 4 + .../services/desktopNotificationService.ts | 74 +++++++++++++++++++ 10 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 frontend/src/core/services/desktopNotificationService.ts create mode 100644 frontend/src/desktop/services/desktopNotificationService.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2422a3cf19..9eaaaf6dd5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -54,6 +54,7 @@ "@tauri-apps/plugin-dialog": "^2.6.0", "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-http": "^2.5.7", + "@tauri-apps/plugin-notification": "^2.3.3", "@tauri-apps/plugin-shell": "^2.3.5", "autoprefixer": "^10.4.21", "axios": "^1.13.2", @@ -4272,6 +4273,15 @@ "@tauri-apps/api": "^2.10.1" } }, + "node_modules/@tauri-apps/plugin-notification": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz", + "integrity": "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, "node_modules/@tauri-apps/plugin-shell": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 211704e4ad..8e2bfbfbd5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,6 +50,7 @@ "@tauri-apps/plugin-dialog": "^2.6.0", "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-http": "^2.5.7", + "@tauri-apps/plugin-notification": "^2.3.3", "@tauri-apps/plugin-shell": "^2.3.5", "autoprefixer": "^10.4.21", "axios": "^1.13.2", diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 390693ad5a..a1aa8b99f6 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -96,6 +96,8 @@ pin = "Pin File (keep active after tool run)" poweredBy = "Powered by" pro = "Pro" processTimeWarning = "Warning: This process can take up to a minute depending on file-size" +processingComplete = "Your file is ready." +processingCompleteMultiple = "{{count}} files are ready." property = "Property" quickPosition = "Quick Position" red = "Red" diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index e48b8ad7c1..a0068e1a5b 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -2268,6 +2268,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac-notification-sys" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" +dependencies = [ + "cc", + "objc2", + "objc2-foundation", + "time", +] + [[package]] name = "markup5ever" version = "0.14.1" @@ -2404,6 +2416,20 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "notify-rust" +version = "4.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -2959,7 +2985,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", "indexmap 2.13.0", - "quick-xml", + "quick-xml 0.38.4", "serde", "time", ] @@ -3135,6 +3161,15 @@ dependencies = [ "psl-types", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -4167,6 +4202,7 @@ dependencies = [ "tauri-plugin-fs", "tauri-plugin-http", "tauri-plugin-log", + "tauri-plugin-notification", "tauri-plugin-opener", "tauri-plugin-shell", "tauri-plugin-single-instance", @@ -4604,6 +4640,25 @@ dependencies = [ "time", ] +[[package]] +name = "tauri-plugin-notification" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" +dependencies = [ + "log", + "notify-rust", + "rand 0.9.2", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", + "url", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.3" @@ -4795,6 +4850,18 @@ dependencies = [ "toml 0.9.12+spec-1.1.0", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml 0.37.5", + "thiserror 2.0.18", + "windows 0.61.3", + "windows-version", +] + [[package]] name = "tempfile" version = "3.26.0" diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 5d5269694e..a730e1fc6a 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -34,6 +34,7 @@ tauri-plugin-single-instance = { version = "2.3.7", features = ["deep-link"] } tauri-plugin-store = "2.4.2" tauri-plugin-opener = "2.5.3" tauri-plugin-deep-link = "2.4.6" +tauri-plugin-notification = "2.3.3" tauri-plugin-window-state = "2.2.1" keyring = { version = "3.6.1", features = ["apple-native", "windows-native"] } tokio = { version = "1.0", features = ["time", "sync"] } diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index 1169ae57f7..6acaac5145 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -55,6 +55,9 @@ "dialog:allow-save", "opener:default", "shell:allow-open", + "notification:allow-notify", + "notification:allow-request-permission", + "notification:allow-is-permission-granted", "window-state:allow-filename", "window-state:allow-restore-state", "window-state:allow-save-window-state" diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 1939b3198a..bac8a25283 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -60,6 +60,7 @@ pub fn run() { .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_store::Builder::new().build()) .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_window_state::Builder::default().build()) .manage(AppConnectionState::default()) .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { diff --git a/frontend/src/core/hooks/tools/shared/useToolOperation.ts b/frontend/src/core/hooks/tools/shared/useToolOperation.ts index 54ad226d8e..78d71f74a3 100644 --- a/frontend/src/core/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/core/hooks/tools/shared/useToolOperation.ts @@ -17,6 +17,7 @@ import { ToolId } from '@app/types/toolId'; import { ensureBackendReady } from '@app/services/backendReadinessGuard'; import { useWillUseCloud } from '@app/hooks/useWillUseCloud'; import { useCreditCheck } from '@app/hooks/useCreditCheck'; +import { notifyPdfProcessingComplete } from '@app/services/desktopNotificationService'; // Re-export for backwards compatibility export type { ProcessingProgress, ResponseHandler }; @@ -476,6 +477,9 @@ export const useToolOperation = ( console.debug('[useToolOperation] Consuming files', { inputCount: inputFileIds.length, toConsume: toConsumeInputIds.length }); const outputFileIds = await consumeFiles(toConsumeInputIds, outputStirlingFiles, outputStirlingFileStubs); + // Notify on desktop when processing completes + await notifyPdfProcessingComplete(outputFileIds.length); + if (toConsumeInputIds.length === 1 && outputFileIds.length === 1) { const inputStub = selectors.getStirlingFileStub(toConsumeInputIds[0]); if (inputStub?.localFilePath) { diff --git a/frontend/src/core/services/desktopNotificationService.ts b/frontend/src/core/services/desktopNotificationService.ts new file mode 100644 index 0000000000..fb3b47d0c7 --- /dev/null +++ b/frontend/src/core/services/desktopNotificationService.ts @@ -0,0 +1,4 @@ +// Stub - overridden in desktop builds +export async function notifyPdfProcessingComplete(_fileCount: number): Promise { + // Web builds: no-op +} diff --git a/frontend/src/desktop/services/desktopNotificationService.ts b/frontend/src/desktop/services/desktopNotificationService.ts new file mode 100644 index 0000000000..5c9a14414c --- /dev/null +++ b/frontend/src/desktop/services/desktopNotificationService.ts @@ -0,0 +1,74 @@ +import { isTauri } from '@tauri-apps/api/core'; +import { getCurrentWindow } from '@tauri-apps/api/window'; +import { + isPermissionGranted, + requestPermission, + sendNotification, +} from '@tauri-apps/plugin-notification'; +import i18n from '@app/i18n'; + +const APP_TITLE = 'Stirling-PDF'; + +async function shouldShowBackgroundNotification(): Promise { + if (!isTauri()) { + return false; + } + + try { + const window = getCurrentWindow(); + const [isMinimized, isFocused] = await Promise.all([ + window.isMinimized().catch(() => false), + window.isFocused().catch(() => true), + ]); + + return isMinimized || !isFocused || document.visibilityState !== 'visible'; + } catch { + return false; + } +} + +export async function notifyPdfProcessingComplete(fileCount: number): Promise { + console.log('[DesktopNotification] notifyPdfProcessingComplete called with fileCount:', fileCount); + + if (!isTauri() || fileCount <= 0) { + console.log('[DesktopNotification] Skipped: !isTauri() or fileCount <= 0'); + return; + } + + const canNotify = await shouldShowBackgroundNotification(); + console.log('[DesktopNotification] canNotify (background):', canNotify); + if (!canNotify) { + console.log('[DesktopNotification] App is in focus, skipping notification'); + return; + } + + try { + // Check and request permission if needed + let permissionGranted = await isPermissionGranted(); + console.log('[DesktopNotification] Permission check:', permissionGranted); + + if (!permissionGranted) { + console.log('[DesktopNotification] Requesting permission...'); + const permission = await requestPermission(); + permissionGranted = permission === 'granted'; + console.log('[DesktopNotification] Permission result:', permission); + } + + if (!permissionGranted) { + console.log('[DesktopNotification] Permission not granted, skipping notification'); + return; + } + + const body = fileCount === 1 + ? i18n.t('processingComplete', 'Your file is ready.') + : i18n.t('processingCompleteMultiple', '{{count}} files are ready.', { count: fileCount }); + console.log('[DesktopNotification] Sending notification:', body); + await sendNotification({ + title: APP_TITLE, + body, + }); + console.log('[DesktopNotification] Notification sent successfully'); + } catch (error) { + console.warn('[DesktopNotification] Unable to send notification', error); + } +}