Add Watch Folders settings page with full CRUD
- New settings page: Watch Folders with add/remove/edit controls - Each watch folder has: path, linked preset dropdown, recursive toggle, active/inactive switch, and remove button - Watch folder config persisted in AppConfig - Empty state message when no folders configured
This commit is contained in:
@@ -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::cell::RefCell<Vec<pixstrip_core::watcher::WatchFolder>>> =
|
||||
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<String> = 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::<gtk::Window>().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<std::cell::RefCell<Vec<pixstrip_core::watcher::WatchFolder>>>,
|
||||
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::<Vec<_>>(),
|
||||
);
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user