feat: expand media discovery and AI workflows #8
+2
-1
@@ -8,7 +8,7 @@
|
||||
"build:vite": "tsc && vite build",
|
||||
"dev:app": "tauri dev",
|
||||
"dev:vite": "vite",
|
||||
"preview": "vite preview",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -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