use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc; use std::sync::Arc; use std::time::Duration; use notify::{Event, EventKind, RecursiveMode, Watcher}; use serde::{Deserialize, Serialize}; use crate::error::{PixstripError, Result}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WatchFolder { pub path: PathBuf, pub preset_name: String, pub recursive: bool, pub active: bool, } pub enum WatchEvent { NewImage(PathBuf), Error(String), } pub struct FolderWatcher { running: Arc, } impl FolderWatcher { pub fn new() -> Self { Self { running: Arc::new(AtomicBool::new(false)), } } pub fn start( &self, folder: &WatchFolder, event_tx: mpsc::Sender, ) -> Result<()> { if !folder.path.exists() { return Err(PixstripError::Config(format!( "Watch folder does not exist: {}", folder.path.display() ))); } self.running.store(true, Ordering::Relaxed); let running = self.running.clone(); let watch_path = folder.path.clone(); let recursive = folder.recursive; std::thread::spawn(move || { let (tx, rx) = mpsc::channel(); let mut watcher = match notify::recommended_watcher(move |res: std::result::Result| { if let Ok(event) = res { let _ = tx.send(event); } }) { Ok(w) => w, Err(e) => { let _ = event_tx.send(WatchEvent::Error(e.to_string())); return; } }; let mode = if recursive { RecursiveMode::Recursive } else { RecursiveMode::NonRecursive }; if let Err(e) = watcher.watch(&watch_path, mode) { let _ = event_tx.send(WatchEvent::Error(e.to_string())); return; } while running.load(Ordering::Relaxed) { match rx.recv_timeout(Duration::from_millis(500)) { Ok(event) => { if matches!(event.kind, EventKind::Create(_)) { for path in event.paths { if is_image_file(&path) { let _ = event_tx.send(WatchEvent::NewImage(path)); } } } } Err(mpsc::RecvTimeoutError::Timeout) => continue, Err(mpsc::RecvTimeoutError::Disconnected) => break, } } }); Ok(()) } pub fn stop(&self) { self.running.store(false, Ordering::Relaxed); } pub fn is_running(&self) -> bool { self.running.load(Ordering::Relaxed) } } impl Default for FolderWatcher { fn default() -> Self { Self::new() } } fn is_image_file(path: &Path) -> bool { let supported = [ "jpg", "jpeg", "png", "webp", "avif", "gif", "tiff", "tif", "bmp", ]; path.extension() .and_then(|e| e.to_str()) .is_some_and(|ext| supported.contains(&ext.to_lowercase().as_str())) }