From e2aee57bd9cda44c0129cc0df098a81daae6abde Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 17:53:41 +0200 Subject: [PATCH] Integrate watch folder monitoring into GTK app Active watch folders from settings are now monitored using the notify crate. When new images appear, they are automatically processed using the folder's linked preset. Toast notifications inform the user. --- pixstrip-gtk/src/app.rs | 108 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/pixstrip-gtk/src/app.rs b/pixstrip-gtk/src/app.rs index 1d26e53..99663f8 100644 --- a/pixstrip-gtk/src/app.rs +++ b/pixstrip-gtk/src/app.rs @@ -659,6 +659,114 @@ fn build_ui(app: &adw::Application) { } } } + + // Start watch folder monitoring for active folders + start_watch_folder_monitoring(&ui); +} + +fn start_watch_folder_monitoring(ui: &WizardUi) { + let config_store = pixstrip_core::storage::ConfigStore::new(); + let config = config_store.load().unwrap_or_default(); + + let active_folders: Vec<_> = config.watch_folders.iter() + .filter(|f| f.active && f.path.exists()) + .cloned() + .collect(); + + if active_folders.is_empty() { + return; + } + + let (tx, rx) = std::sync::mpsc::channel::(); + + // Start a watcher for each active folder + for folder in &active_folders { + let watcher = pixstrip_core::watcher::FolderWatcher::new(); + let folder_tx = tx.clone(); + if let Err(e) = watcher.start(folder, folder_tx) { + eprintln!("Failed to start watching {}: {}", folder.path.display(), e); + continue; + } + // Leak the watcher so it stays alive for the lifetime of the app + std::mem::forget(watcher); + } + + // Build a lookup from folder path to preset name + let folder_presets: std::collections::HashMap = active_folders + .iter() + .map(|f| (f.path.clone(), f.preset_name.clone())) + .collect(); + + let toast_overlay = ui.toast_overlay.clone(); + + // Poll the channel from the main loop + glib::timeout_add_local(std::time::Duration::from_millis(500), move || { + let mut batch: Vec<(std::path::PathBuf, String)> = Vec::new(); + + // Drain all pending events + while let Ok(event) = rx.try_recv() { + match event { + pixstrip_core::watcher::WatchEvent::NewImage(path) => { + // Find which watch folder this belongs to + for (folder_path, preset_name) in &folder_presets { + if path.starts_with(folder_path) { + batch.push((path.clone(), preset_name.clone())); + break; + } + } + } + pixstrip_core::watcher::WatchEvent::Error(e) => { + eprintln!("Watch folder error: {}", e); + } + } + } + + if !batch.is_empty() { + // Group by preset name and process + let preset_store = pixstrip_core::storage::PresetStore::new(); + let mut by_preset: std::collections::HashMap> = + std::collections::HashMap::new(); + for (path, preset) in batch { + by_preset.entry(preset).or_default().push(path); + } + + for (preset_name, files) in by_preset { + if let Ok(presets) = preset_store.list() { + if let Some(preset) = presets.iter().find(|p| p.name == preset_name) { + let count = files.len(); + let preset = preset.clone(); + std::thread::spawn(move || { + // Build output dir next to the first file + let output_dir = files.first() + .and_then(|f| f.parent()) + .map(|p| p.join("processed")) + .unwrap_or_else(|| std::path::PathBuf::from("processed")); + let input_dir = files.first() + .and_then(|f| f.parent()) + .unwrap_or_else(|| std::path::Path::new(".")) + .to_path_buf(); + let mut job = preset.to_job(&input_dir, &output_dir); + for file in &files { + job.add_source(file); + } + let executor = pixstrip_core::executor::PipelineExecutor::new(); + let _ = executor.execute(&job, |_| {}); + }); + + let toast = adw::Toast::new(&format!( + "Watch: processing {} new image{}", + count, + if count == 1 { "" } else { "s" } + )); + toast.set_timeout(3); + toast_overlay.add_toast(toast); + } + } + } + } + + glib::ControlFlow::Continue + }); } fn build_menu() -> gtk::gio::Menu {