feat(notifications): report completed background tasks
Register the Tauri notification plugin and request notification permission during startup. Send completion notifications for folder scans, embeddings, AI tagging, and duplicate scans, including failure counts where available.
This commit is contained in:
+2
-1
@@ -8,7 +8,7 @@
|
|||||||
"build:vite": "tsc && vite build",
|
"build:vite": "tsc && vite build",
|
||||||
"dev:app": "tauri dev",
|
"dev:app": "tauri dev",
|
||||||
"dev:vite": "vite",
|
"dev:vite": "vite",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "^2.7.0",
|
"@tauri-apps/plugin-dialog": "^2.7.0",
|
||||||
"@tauri-apps/plugin-fs": "^2.5.0",
|
"@tauri-apps/plugin-fs": "^2.5.0",
|
||||||
|
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"d3-force": "^3.0.0",
|
"d3-force": "^3.0.0",
|
||||||
"framer-motion": "^12.38.0",
|
"framer-motion": "^12.38.0",
|
||||||
|
|||||||
Generated
+10
@@ -20,6 +20,9 @@ importers:
|
|||||||
'@tauri-apps/plugin-fs':
|
'@tauri-apps/plugin-fs':
|
||||||
specifier: ^2.5.0
|
specifier: ^2.5.0
|
||||||
version: 2.5.0
|
version: 2.5.0
|
||||||
|
'@tauri-apps/plugin-notification':
|
||||||
|
specifier: ^2.3.3
|
||||||
|
version: 2.3.3
|
||||||
'@tauri-apps/plugin-opener':
|
'@tauri-apps/plugin-opener':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.5.3
|
version: 2.5.3
|
||||||
@@ -653,6 +656,9 @@ packages:
|
|||||||
'@tauri-apps/plugin-fs@2.5.0':
|
'@tauri-apps/plugin-fs@2.5.0':
|
||||||
resolution: {integrity: sha512-c83kbz61AK+rKjhS+je9+stIO27nXj7p9cqeg36TwkIUtxpCFTttlHHtqon6h6FN54cXjyAjlMPOJcW3mwE5XQ==}
|
resolution: {integrity: sha512-c83kbz61AK+rKjhS+je9+stIO27nXj7p9cqeg36TwkIUtxpCFTttlHHtqon6h6FN54cXjyAjlMPOJcW3mwE5XQ==}
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-notification@2.3.3':
|
||||||
|
resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==}
|
||||||
|
|
||||||
'@tauri-apps/plugin-opener@2.5.3':
|
'@tauri-apps/plugin-opener@2.5.3':
|
||||||
resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==}
|
resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==}
|
||||||
|
|
||||||
@@ -1448,6 +1454,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.10.1
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-notification@2.3.3':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|
||||||
'@tauri-apps/plugin-opener@2.5.3':
|
'@tauri-apps/plugin-opener@2.5.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.10.1
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|||||||
Generated
+68
-1
@@ -3293,6 +3293,18 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mac-notification-sys"
|
||||||
|
version = "0.6.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50efa634682b3fc5a1ab6f3dd5b2bce7b848011fc485b53b063dc68f2f74feae"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"objc2",
|
||||||
|
"objc2-foundation",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mach2"
|
name = "mach2"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -3594,6 +3606,20 @@ dependencies = [
|
|||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify-rust"
|
||||||
|
version = "4.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50ff2e74231b72c832d82982193b417f230945be6bdb5575b251d941d31adb00"
|
||||||
|
dependencies = [
|
||||||
|
"futures-lite",
|
||||||
|
"log",
|
||||||
|
"mac-notification-sys",
|
||||||
|
"serde",
|
||||||
|
"tauri-winrt-notification",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ntapi"
|
name = "ntapi"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -4279,6 +4305,7 @@ dependencies = [
|
|||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
|
"tauri-plugin-notification",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tokenizers",
|
"tokenizers",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -4326,7 +4353,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"indexmap 2.13.1",
|
"indexmap 2.13.1",
|
||||||
"quick-xml",
|
"quick-xml 0.38.4",
|
||||||
"serde",
|
"serde",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
@@ -4543,6 +4570,15 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.37.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.38.4"
|
version = "0.38.4"
|
||||||
@@ -6016,6 +6052,25 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "tauri-plugin-opener"
|
name = "tauri-plugin-opener"
|
||||||
version = "2.5.3"
|
version = "2.5.3"
|
||||||
@@ -6138,6 +6193,18 @@ dependencies = [
|
|||||||
"toml 0.9.12+spec-1.1.0",
|
"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]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.27.0"
|
version = "3.27.0"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ ort = { version = "2.0.0-rc.12", default-features = false, features = ["std", "n
|
|||||||
ureq = { version = "3.3.0", default-features = false, features = ["native-tls"] }
|
ureq = { version = "3.3.0", default-features = false, features = ["native-tls"] }
|
||||||
zip = { version = "4.6.1", default-features = false, features = ["deflate"] }
|
zip = { version = "4.6.1", default-features = false, features = ["deflate"] }
|
||||||
csv = "1"
|
csv = "1"
|
||||||
|
tauri-plugin-notification = "2"
|
||||||
|
|
||||||
# ── Dev-mode performance ────────────────────────────────────────────────────
|
# ── Dev-mode performance ────────────────────────────────────────────────────
|
||||||
# opt-level=1 on the main crate keeps incremental compile short.
|
# opt-level=1 on the main crate keeps incremental compile short.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"fs:allow-read-dir",
|
"fs:allow-read-dir",
|
||||||
"fs:read-files",
|
"fs:read-files",
|
||||||
"fs:read-dirs",
|
"fs:read-dirs",
|
||||||
|
"notification:default",
|
||||||
"core:window:allow-minimize",
|
"core:window:allow-minimize",
|
||||||
"core:window:allow-close",
|
"core:window:allow-close",
|
||||||
"core:window:allow-toggle-maximize",
|
"core:window:allow-toggle-maximize",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
|
.plugin(tauri_plugin_notification::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let app_dir = app
|
let app_dir = app
|
||||||
.path()
|
.path()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { TagCloud } from "./components/TagCloud";
|
|||||||
import { DuplicateFinder } from "./components/DuplicateFinder";
|
import { DuplicateFinder } from "./components/DuplicateFinder";
|
||||||
import { TitleBar } from "./components/TitleBar";
|
import { TitleBar } from "./components/TitleBar";
|
||||||
import { SettingsModal } from "./components/SettingsModal";
|
import { SettingsModal } from "./components/SettingsModal";
|
||||||
|
import { initializeNotifications } from "./notifications";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const loadFolders = useGalleryStore((state) => state.loadFolders);
|
const loadFolders = useGalleryStore((state) => state.loadFolders);
|
||||||
@@ -20,6 +21,7 @@ export default function App() {
|
|||||||
const activeView = useGalleryStore((state) => state.activeView);
|
const activeView = useGalleryStore((state) => state.activeView);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
void initializeNotifications();
|
||||||
loadFolders().then(() => {
|
loadFolders().then(() => {
|
||||||
void loadBackgroundJobProgress();
|
void loadBackgroundJobProgress();
|
||||||
void loadCaptionModelStatus();
|
void loadCaptionModelStatus();
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
isPermissionGranted,
|
||||||
|
requestPermission,
|
||||||
|
sendNotification,
|
||||||
|
} from "@tauri-apps/plugin-notification";
|
||||||
|
|
||||||
|
let permissionPromise: Promise<boolean> | null = null;
|
||||||
|
|
||||||
|
export function initializeNotifications(): Promise<boolean> {
|
||||||
|
permissionPromise ??= (async () => {
|
||||||
|
try {
|
||||||
|
if (await isPermissionGranted()) return true;
|
||||||
|
return (await requestPermission()) === "granted";
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Windows notifications are unavailable:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return permissionPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function notifyTaskComplete(title: string, body: string): Promise<void> {
|
||||||
|
if (!(await initializeNotifications())) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
sendNotification({ title, body });
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Could not send task completion notification:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { create } from "zustand";
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||||
import { appDataDir, join } from "@tauri-apps/api/path";
|
import { appDataDir, join } from "@tauri-apps/api/path";
|
||||||
|
import { notifyTaskComplete } from "./notifications";
|
||||||
|
|
||||||
export interface Folder {
|
export interface Folder {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -1394,6 +1395,10 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
try {
|
try {
|
||||||
const groups = await invoke<DuplicateGroup[]>("find_duplicates", { folderId: folderId ?? null });
|
const groups = await invoke<DuplicateGroup[]>("find_duplicates", { folderId: folderId ?? null });
|
||||||
set({ duplicateGroups: groups, duplicateLastScanned: Math.floor(Date.now() / 1000) });
|
set({ duplicateGroups: groups, duplicateLastScanned: Math.floor(Date.now() / 1000) });
|
||||||
|
void notifyTaskComplete(
|
||||||
|
"Duplicate scan complete",
|
||||||
|
groups.length === 1 ? "Found 1 duplicate group." : `Found ${groups.length.toLocaleString()} duplicate groups.`,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
unlisten();
|
unlisten();
|
||||||
set({ duplicateScanning: false });
|
set({ duplicateScanning: false });
|
||||||
@@ -1466,6 +1471,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
subscribeToProgress: async () => {
|
subscribeToProgress: async () => {
|
||||||
const unlistenProgress = await listen<IndexProgress>("index-progress", (event) => {
|
const unlistenProgress = await listen<IndexProgress>("index-progress", (event) => {
|
||||||
const progress = event.payload;
|
const progress = event.payload;
|
||||||
|
const previous = get().indexingProgress[progress.folder_id];
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
indexingProgress: {
|
indexingProgress: {
|
||||||
...state.indexingProgress,
|
...state.indexingProgress,
|
||||||
@@ -1474,6 +1480,18 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
if (progress.done) {
|
if (progress.done) {
|
||||||
|
if (
|
||||||
|
previous &&
|
||||||
|
!previous.done &&
|
||||||
|
progress.total > 0 &&
|
||||||
|
progress.indexed >= progress.total
|
||||||
|
) {
|
||||||
|
const folderName = get().folders.find((folder) => folder.id === progress.folder_id)?.name;
|
||||||
|
void notifyTaskComplete(
|
||||||
|
"Folder scan complete",
|
||||||
|
folderName ? `${folderName} has finished scanning.` : "A folder has finished scanning.",
|
||||||
|
);
|
||||||
|
}
|
||||||
void get().loadFolders();
|
void get().loadFolders();
|
||||||
void get().loadBackgroundJobProgress();
|
void get().loadBackgroundJobProgress();
|
||||||
if (get().activeView !== "explore" && !isDerivedCollectionTitle(get().collectionTitle)) {
|
if (get().activeView !== "explore" && !isDerivedCollectionTitle(get().collectionTitle)) {
|
||||||
@@ -1491,6 +1509,38 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const unlistenMediaJobs = await listen<MediaJobProgressEvent>("media-job-progress", (event) => {
|
const unlistenMediaJobs = await listen<MediaJobProgressEvent>("media-job-progress", (event) => {
|
||||||
|
const previousProgress = get().mediaJobProgress;
|
||||||
|
|
||||||
|
for (const progress of event.payload.progress) {
|
||||||
|
const previous = previousProgress[progress.folder_id];
|
||||||
|
if (!previous) continue;
|
||||||
|
|
||||||
|
const folderName =
|
||||||
|
get().folders.find((folder) => folder.id === progress.folder_id)?.name ?? "Folder";
|
||||||
|
|
||||||
|
if (previous.embedding_pending > 0 && progress.embedding_pending === 0) {
|
||||||
|
const failureDetail =
|
||||||
|
progress.embedding_failed > 0
|
||||||
|
? ` ${progress.embedding_failed.toLocaleString()} failed.`
|
||||||
|
: "";
|
||||||
|
void notifyTaskComplete(
|
||||||
|
"Embeddings complete",
|
||||||
|
`${folderName} finished generating embeddings.${failureDetail}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous.tagging_pending > 0 && progress.tagging_pending === 0) {
|
||||||
|
const failureDetail =
|
||||||
|
progress.tagging_failed > 0
|
||||||
|
? ` ${progress.tagging_failed.toLocaleString()} failed.`
|
||||||
|
: "";
|
||||||
|
void notifyTaskComplete(
|
||||||
|
"AI tagging complete",
|
||||||
|
`${folderName} finished generating tags.${failureDetail}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const next = { ...state.mediaJobProgress };
|
const next = { ...state.mediaJobProgress };
|
||||||
for (const progress of event.payload.progress) {
|
for (const progress of event.payload.progress) {
|
||||||
|
|||||||
Reference in New Issue
Block a user