diff --git a/pixstrip-core/src/config.rs b/pixstrip-core/src/config.rs index aacc032..04d7ad3 100644 --- a/pixstrip-core/src/config.rs +++ b/pixstrip-core/src/config.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use crate::watcher::WatchFolder; + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] pub struct AppConfig { @@ -20,6 +22,7 @@ pub struct AppConfig { pub reduced_motion: bool, pub history_max_entries: usize, pub history_max_days: u32, + pub watch_folders: Vec, } impl Default for AppConfig { @@ -42,6 +45,7 @@ impl Default for AppConfig { reduced_motion: false, history_max_entries: 50, history_max_days: 30, + watch_folders: Vec::new(), } } } diff --git a/pixstrip-gtk/src/settings.rs b/pixstrip-gtk/src/settings.rs index 110c71e..7f55c44 100644 --- a/pixstrip-gtk/src/settings.rs +++ b/pixstrip-gtk/src/settings.rs @@ -252,6 +252,106 @@ pub fn build_settings_dialog() -> adw::PreferencesDialog { notify_page.add(¬ify_group); dialog.add(¬ify_page); + // Watch Folders page + let watch_page = adw::PreferencesPage::builder() + .title("Watch Folders") + .icon_name("folder-visiting-symbolic") + .build(); + + let watch_group = adw::PreferencesGroup::builder() + .title("Monitored Folders") + .description("Automatically process images added to these folders") + .build(); + + let watch_list = gtk::ListBox::builder() + .selection_mode(gtk::SelectionMode::None) + .css_classes(["boxed-list"]) + .build(); + watch_list.set_widget_name("watch-folder-list"); + + // Shared state for watch folders + let watch_folders_state: std::rc::Rc>> = + std::rc::Rc::new(std::cell::RefCell::new(config.watch_folders.clone())); + + // Build preset names list for dropdown + let builtin_presets = pixstrip_core::preset::Preset::all_builtins(); + let preset_names: Vec = builtin_presets.iter().map(|p| p.name.clone()).collect(); + + // Populate existing watch folders + { + let folders = watch_folders_state.borrow(); + for folder in folders.iter() { + let row = build_watch_folder_row(folder, &preset_names, &watch_folders_state, &watch_list); + watch_list.append(&row); + } + } + + // Empty state + let empty_label = gtk::Label::builder() + .label("No watch folders configured.\nAdd a folder to start automatic processing.") + .css_classes(["dim-label"]) + .halign(gtk::Align::Center) + .margin_top(16) + .margin_bottom(16) + .justify(gtk::Justification::Center) + .build(); + empty_label.set_visible(config.watch_folders.is_empty()); + + watch_group.add(&watch_list); + watch_group.add(&empty_label); + + // Add folder button + let add_button = gtk::Button::builder() + .label("Add Watch Folder") + .halign(gtk::Align::Start) + .margin_top(8) + .build(); + add_button.add_css_class("suggested-action"); + add_button.add_css_class("pill"); + + { + let wfs = watch_folders_state.clone(); + let wl = watch_list.clone(); + let el = empty_label.clone(); + let pnames = preset_names.clone(); + add_button.connect_clicked(move |btn| { + let wfs = wfs.clone(); + let wl = wl.clone(); + let el = el.clone(); + let pnames = pnames.clone(); + let dialog = gtk::FileDialog::builder() + .title("Choose Watch Folder") + .modal(true) + .build(); + + if let Some(window) = btn.root().and_then(|r| r.downcast::().ok()) { + dialog.select_folder(Some(&window), gtk::gio::Cancellable::NONE, move |result| { + if let Ok(file) = result + && let Some(path) = file.path() + { + let new_folder = pixstrip_core::watcher::WatchFolder { + path: path.clone(), + preset_name: "Blog Photos".to_string(), + recursive: false, + active: true, + }; + let row = build_watch_folder_row(&new_folder, &pnames, &wfs, &wl); + wl.append(&row); + wfs.borrow_mut().push(new_folder); + el.set_visible(false); + } + }); + } + }); + } + + let add_group = adw::PreferencesGroup::new(); + add_group.add(&add_button); + + watch_page.add(&watch_group); + watch_page.add(&add_group); + dialog.add(&watch_page); + // Wire reset button { let subfolder = subfolder_row.clone(); @@ -324,6 +424,7 @@ pub fn build_settings_dialog() -> adw::PreferencesDialog { reduced_motion: motion_row.is_active(), history_max_entries: hist_max_entries, history_max_days: hist_max_days, + watch_folders: watch_folders_state.borrow().clone(), }; let store = ConfigStore::new(); @@ -332,3 +433,117 @@ pub fn build_settings_dialog() -> adw::PreferencesDialog { dialog } + +fn build_watch_folder_row( + folder: &pixstrip_core::watcher::WatchFolder, + preset_names: &[String], + watch_state: &std::rc::Rc>>, + list_box: >k::ListBox, +) -> adw::ExpanderRow { + let display_path = folder.path.file_name() + .and_then(|n| n.to_str()) + .unwrap_or_else(|| folder.path.to_str().unwrap_or("Unknown")); + + let row = adw::ExpanderRow::builder() + .title(display_path) + .subtitle(&folder.path.display().to_string()) + .show_enable_switch(true) + .enable_expansion(folder.active) + .build(); + row.add_prefix(>k::Image::from_icon_name("folder-visiting-symbolic")); + + // Preset selector + let preset_row = adw::ComboRow::builder() + .title("Linked Preset") + .subtitle("Preset to apply to new images") + .build(); + let preset_model = gtk::StringList::new( + &preset_names.iter().map(|s| s.as_str()).collect::>(), + ); + preset_row.set_model(Some(&preset_model)); + + // Set selected to matching preset + let selected_idx = preset_names.iter() + .position(|n| *n == folder.preset_name) + .unwrap_or(0); + preset_row.set_selected(selected_idx as u32); + + // Recursive toggle + let recursive_row = adw::SwitchRow::builder() + .title("Include Subfolders") + .subtitle("Monitor subfolders recursively") + .active(folder.recursive) + .build(); + + // Remove button + let remove_row = adw::ActionRow::builder() + .title("Remove This Folder") + .build(); + remove_row.add_prefix(>k::Image::from_icon_name("user-trash-symbolic")); + let remove_btn = gtk::Button::builder() + .icon_name("edit-delete-symbolic") + .tooltip_text("Remove watch folder") + .valign(gtk::Align::Center) + .build(); + remove_btn.add_css_class("flat"); + remove_btn.add_css_class("error"); + remove_row.add_suffix(&remove_btn); + + row.add_row(&preset_row); + row.add_row(&recursive_row); + row.add_row(&remove_row); + + // Wire enable toggle + let folder_path = folder.path.clone(); + { + let wfs = watch_state.clone(); + let fp = folder_path.clone(); + row.connect_enable_expansion_notify(move |r| { + let mut folders = wfs.borrow_mut(); + if let Some(f) = folders.iter_mut().find(|f| f.path == fp) { + f.active = r.enables_expansion(); + } + }); + } + + // Wire preset change + { + let wfs = watch_state.clone(); + let fp = folder_path.clone(); + let pnames = preset_names.to_vec(); + preset_row.connect_selected_notify(move |r| { + let mut folders = wfs.borrow_mut(); + if let Some(f) = folders.iter_mut().find(|f| f.path == fp) { + f.preset_name = pnames.get(r.selected() as usize) + .cloned() + .unwrap_or_default(); + } + }); + } + + // Wire recursive toggle + { + let wfs = watch_state.clone(); + let fp = folder_path.clone(); + recursive_row.connect_active_notify(move |r| { + let mut folders = wfs.borrow_mut(); + if let Some(f) = folders.iter_mut().find(|f| f.path == fp) { + f.recursive = r.is_active(); + } + }); + } + + // Wire remove button + { + let wfs = watch_state.clone(); + let lb = list_box.clone(); + let fp = folder_path; + let r = row.clone(); + remove_btn.connect_clicked(move |_| { + wfs.borrow_mut().retain(|f| f.path != fp); + lb.remove(&r); + }); + } + + row +}