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.
This commit is contained in:
2026-03-06 17:53:41 +02:00
parent afabdf3548
commit e2aee57bd9

View File

@@ -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::<pixstrip_core::watcher::WatchEvent>();
// 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<std::path::PathBuf, String> = 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<String, Vec<std::path::PathBuf>> =
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 { fn build_menu() -> gtk::gio::Menu {