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:
@@ -16,6 +16,7 @@
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "^2.7.0",
|
||||
"@tauri-apps/plugin-fs": "^2.5.0",
|
||||
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"d3-force": "^3.0.0",
|
||||
"framer-motion": "^12.38.0",
|
||||
|
||||
Generated
+10
@@ -20,6 +20,9 @@ importers:
|
||||
'@tauri-apps/plugin-fs':
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
'@tauri-apps/plugin-notification':
|
||||
specifier: ^2.3.3
|
||||
version: 2.3.3
|
||||
'@tauri-apps/plugin-opener':
|
||||
specifier: ^2
|
||||
version: 2.5.3
|
||||
@@ -653,6 +656,9 @@ packages:
|
||||
'@tauri-apps/plugin-fs@2.5.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==}
|
||||
|
||||
@@ -1448,6 +1454,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@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':
|
||||
dependencies:
|
||||
'@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"
|
||||
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]]
|
||||
name = "mach2"
|
||||
version = "0.4.3"
|
||||
@@ -3594,6 +3606,20 @@ dependencies = [
|
||||
"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]]
|
||||
name = "ntapi"
|
||||
version = "0.4.3"
|
||||
@@ -4279,6 +4305,7 @@ dependencies = [
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-opener",
|
||||
"tokenizers",
|
||||
"tokio",
|
||||
@@ -4326,7 +4353,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"indexmap 2.13.1",
|
||||
"quick-xml",
|
||||
"quick-xml 0.38.4",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
@@ -4543,6 +4570,15 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.4"
|
||||
@@ -6016,6 +6052,25 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.5.3"
|
||||
@@ -6138,6 +6193,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.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"] }
|
||||
zip = { version = "4.6.1", default-features = false, features = ["deflate"] }
|
||||
csv = "1"
|
||||
tauri-plugin-notification = "2"
|
||||
|
||||
# ── Dev-mode performance ────────────────────────────────────────────────────
|
||||
# opt-level=1 on the main crate keeps incremental compile short.
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"fs:allow-read-dir",
|
||||
"fs:read-files",
|
||||
"fs:read-dirs",
|
||||
"notification:default",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-toggle-maximize",
|
||||
|
||||
@@ -19,6 +19,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.setup(|app| {
|
||||
let app_dir = app
|
||||
.path()
|
||||
|
||||
@@ -9,6 +9,7 @@ import { TagCloud } from "./components/TagCloud";
|
||||
import { DuplicateFinder } from "./components/DuplicateFinder";
|
||||
import { TitleBar } from "./components/TitleBar";
|
||||
import { SettingsModal } from "./components/SettingsModal";
|
||||
import { initializeNotifications } from "./notifications";
|
||||
|
||||
export default function App() {
|
||||
const loadFolders = useGalleryStore((state) => state.loadFolders);
|
||||
@@ -20,6 +21,7 @@ export default function App() {
|
||||
const activeView = useGalleryStore((state) => state.activeView);
|
||||
|
||||
useEffect(() => {
|
||||
void initializeNotifications();
|
||||
loadFolders().then(() => {
|
||||
void loadBackgroundJobProgress();
|
||||
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 { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { appDataDir, join } from "@tauri-apps/api/path";
|
||||
import { notifyTaskComplete } from "./notifications";
|
||||
|
||||
export interface Folder {
|
||||
id: number;
|
||||
@@ -1394,6 +1395,10 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
||||
try {
|
||||
const groups = await invoke<DuplicateGroup[]>("find_duplicates", { folderId: folderId ?? null });
|
||||
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 {
|
||||
unlisten();
|
||||
set({ duplicateScanning: false });
|
||||
@@ -1466,6 +1471,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
||||
subscribeToProgress: async () => {
|
||||
const unlistenProgress = await listen<IndexProgress>("index-progress", (event) => {
|
||||
const progress = event.payload;
|
||||
const previous = get().indexingProgress[progress.folder_id];
|
||||
set((state) => ({
|
||||
indexingProgress: {
|
||||
...state.indexingProgress,
|
||||
@@ -1474,6 +1480,18 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
||||
}));
|
||||
|
||||
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().loadBackgroundJobProgress();
|
||||
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 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) => {
|
||||
const next = { ...state.mediaJobProgress };
|
||||
for (const progress of event.payload.progress) {
|
||||
|
||||
Reference in New Issue
Block a user