Users can now choose between subfolder-next-to-originals or a fixed output folder in Settings > General. The fixed path is selectable via a folder picker dialog and persisted across sessions.
633 lines
22 KiB
Rust
633 lines
22 KiB
Rust
use adw::prelude::*;
|
|
use pixstrip_core::config::{AppConfig, ErrorBehavior, OverwriteBehavior, SkillLevel};
|
|
use pixstrip_core::storage::ConfigStore;
|
|
|
|
pub fn build_settings_dialog() -> adw::PreferencesDialog {
|
|
let dialog = adw::PreferencesDialog::builder()
|
|
.title("Settings")
|
|
.build();
|
|
|
|
let config_store = ConfigStore::new();
|
|
let config = config_store.load().unwrap_or_default();
|
|
|
|
// General page
|
|
let general_page = adw::PreferencesPage::builder()
|
|
.title("General")
|
|
.icon_name("preferences-system-symbolic")
|
|
.build();
|
|
|
|
let output_group = adw::PreferencesGroup::builder()
|
|
.title("Output")
|
|
.build();
|
|
|
|
// Output mode: subfolder or fixed path
|
|
let output_mode_row = adw::ComboRow::builder()
|
|
.title("Default output location")
|
|
.subtitle("Where processed images are saved by default")
|
|
.build();
|
|
let output_mode_model = gtk::StringList::new(&[
|
|
"Subfolder next to originals",
|
|
"Fixed output folder",
|
|
]);
|
|
output_mode_row.set_model(Some(&output_mode_model));
|
|
output_mode_row.set_selected(if config.output_fixed_path.is_some() { 1 } else { 0 });
|
|
|
|
let subfolder_row = adw::EntryRow::builder()
|
|
.title("Default output subfolder")
|
|
.text(&config.output_subfolder)
|
|
.visible(config.output_fixed_path.is_none())
|
|
.build();
|
|
|
|
let fixed_path_row = adw::ActionRow::builder()
|
|
.title("Fixed output folder")
|
|
.subtitle(
|
|
config.output_fixed_path
|
|
.as_deref()
|
|
.unwrap_or("No folder selected"),
|
|
)
|
|
.activatable(true)
|
|
.visible(config.output_fixed_path.is_some())
|
|
.build();
|
|
fixed_path_row.add_prefix(>k::Image::from_icon_name("folder-open-symbolic"));
|
|
|
|
let choose_fixed_btn = gtk::Button::builder()
|
|
.icon_name("document-open-symbolic")
|
|
.tooltip_text("Choose output folder")
|
|
.valign(gtk::Align::Center)
|
|
.build();
|
|
choose_fixed_btn.add_css_class("flat");
|
|
fixed_path_row.add_suffix(&choose_fixed_btn);
|
|
|
|
// Shared state for fixed path
|
|
let fixed_path_state: std::rc::Rc<std::cell::RefCell<Option<String>>> =
|
|
std::rc::Rc::new(std::cell::RefCell::new(config.output_fixed_path.clone()));
|
|
|
|
// Wire output mode toggle
|
|
{
|
|
let sf = subfolder_row.clone();
|
|
let fp = fixed_path_row.clone();
|
|
output_mode_row.connect_selected_notify(move |row| {
|
|
let is_fixed = row.selected() == 1;
|
|
sf.set_visible(!is_fixed);
|
|
fp.set_visible(is_fixed);
|
|
});
|
|
}
|
|
|
|
// Wire fixed path chooser
|
|
{
|
|
let fps = fixed_path_state.clone();
|
|
let fpr = fixed_path_row.clone();
|
|
choose_fixed_btn.connect_clicked(move |btn| {
|
|
let fps = fps.clone();
|
|
let fpr = fpr.clone();
|
|
let dialog = gtk::FileDialog::builder()
|
|
.title("Choose Output 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 path_str = path.display().to_string();
|
|
fpr.set_subtitle(&path_str);
|
|
*fps.borrow_mut() = Some(path_str);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
let overwrite_row = adw::ComboRow::builder()
|
|
.title("Default overwrite behavior")
|
|
.subtitle("What to do when output files already exist")
|
|
.build();
|
|
let overwrite_model = gtk::StringList::new(&[
|
|
"Ask before overwriting",
|
|
"Auto-rename with suffix",
|
|
"Always overwrite",
|
|
"Skip existing files",
|
|
]);
|
|
overwrite_row.set_model(Some(&overwrite_model));
|
|
overwrite_row.set_selected(match config.overwrite_behavior {
|
|
OverwriteBehavior::Ask => 0,
|
|
OverwriteBehavior::AutoRename => 1,
|
|
OverwriteBehavior::Overwrite => 2,
|
|
OverwriteBehavior::Skip => 3,
|
|
});
|
|
|
|
let remember_row = adw::SwitchRow::builder()
|
|
.title("Remember last-used settings")
|
|
.subtitle("Restore wizard state on next launch")
|
|
.active(config.remember_settings)
|
|
.build();
|
|
|
|
output_group.add(&output_mode_row);
|
|
output_group.add(&subfolder_row);
|
|
output_group.add(&fixed_path_row);
|
|
output_group.add(&overwrite_row);
|
|
output_group.add(&remember_row);
|
|
general_page.add(&output_group);
|
|
|
|
let ui_group = adw::PreferencesGroup::builder()
|
|
.title("Interface")
|
|
.build();
|
|
|
|
let skill_row = adw::ComboRow::builder()
|
|
.title("Detail level")
|
|
.subtitle("Controls how many options are visible by default")
|
|
.build();
|
|
let skill_model = gtk::StringList::new(&["Simple", "Detailed"]);
|
|
skill_row.set_model(Some(&skill_model));
|
|
skill_row.set_selected(match config.skill_level {
|
|
SkillLevel::Simple => 0,
|
|
SkillLevel::Detailed => 1,
|
|
});
|
|
|
|
let reset_button = gtk::Button::builder()
|
|
.label("Reset to Defaults")
|
|
.halign(gtk::Align::Start)
|
|
.margin_top(8)
|
|
.build();
|
|
reset_button.add_css_class("destructive-action");
|
|
|
|
ui_group.add(&skill_row);
|
|
general_page.add(&ui_group);
|
|
|
|
// File Manager Integration
|
|
let fm_group = adw::PreferencesGroup::builder()
|
|
.title("File Manager Integration")
|
|
.description("Add 'Process with Pixstrip' to your file manager's right-click menu")
|
|
.build();
|
|
|
|
use pixstrip_core::fm_integration::FileManager;
|
|
let file_managers = [
|
|
(FileManager::Nautilus, "org.gnome.Nautilus"),
|
|
(FileManager::Nemo, "org.nemo.Nemo"),
|
|
(FileManager::Thunar, "thunar"),
|
|
(FileManager::Dolphin, "org.kde.dolphin"),
|
|
];
|
|
|
|
let mut found_fm = false;
|
|
for (fm, desktop_id) in &file_managers {
|
|
let is_installed = gtk::gio::AppInfo::all()
|
|
.iter()
|
|
.any(|info| {
|
|
info.id()
|
|
.map(|id| id.as_str().contains(desktop_id))
|
|
.unwrap_or(false)
|
|
});
|
|
|
|
if is_installed {
|
|
found_fm = true;
|
|
let already_installed = fm.is_installed();
|
|
let row = adw::SwitchRow::builder()
|
|
.title(fm.name())
|
|
.subtitle(format!("Add right-click menu to {}", fm.name()))
|
|
.active(already_installed)
|
|
.build();
|
|
|
|
let fm_copy = *fm;
|
|
row.connect_active_notify(move |row| {
|
|
if row.is_active() {
|
|
let _ = fm_copy.install();
|
|
} else {
|
|
let _ = fm_copy.uninstall();
|
|
}
|
|
});
|
|
|
|
fm_group.add(&row);
|
|
}
|
|
}
|
|
|
|
if !found_fm {
|
|
let row = adw::ActionRow::builder()
|
|
.title("No supported file managers detected")
|
|
.subtitle("Nautilus, Nemo, Thunar, and Dolphin are supported")
|
|
.build();
|
|
row.add_prefix(>k::Image::from_icon_name("dialog-information-symbolic"));
|
|
fm_group.add(&row);
|
|
}
|
|
|
|
general_page.add(&fm_group);
|
|
|
|
// Reset defaults group
|
|
let reset_group = adw::PreferencesGroup::new();
|
|
reset_group.add(&reset_button);
|
|
general_page.add(&reset_group);
|
|
|
|
dialog.add(&general_page);
|
|
|
|
// Processing page
|
|
let processing_page = adw::PreferencesPage::builder()
|
|
.title("Processing")
|
|
.icon_name("system-run-symbolic")
|
|
.build();
|
|
|
|
let threads_group = adw::PreferencesGroup::builder()
|
|
.title("Performance")
|
|
.build();
|
|
|
|
let threads_row = adw::ComboRow::builder()
|
|
.title("Processing threads")
|
|
.subtitle("Auto uses all available CPU cores")
|
|
.build();
|
|
let threads_model = gtk::StringList::new(&["Auto", "1", "2", "4", "8"]);
|
|
threads_row.set_model(Some(&threads_model));
|
|
threads_row.set_selected(match config.thread_count {
|
|
pixstrip_core::config::ThreadCount::Auto => 0,
|
|
pixstrip_core::config::ThreadCount::Manual(1) => 1,
|
|
pixstrip_core::config::ThreadCount::Manual(2) => 2,
|
|
pixstrip_core::config::ThreadCount::Manual(n) if n <= 4 => 3,
|
|
pixstrip_core::config::ThreadCount::Manual(_) => 4,
|
|
});
|
|
|
|
let error_row = adw::ComboRow::builder()
|
|
.title("On error")
|
|
.subtitle("What to do when an image fails to process")
|
|
.build();
|
|
let error_model = gtk::StringList::new(&["Skip and continue", "Pause on error"]);
|
|
error_row.set_model(Some(&error_model));
|
|
error_row.set_selected(match config.error_behavior {
|
|
ErrorBehavior::SkipAndContinue => 0,
|
|
ErrorBehavior::PauseOnError => 1,
|
|
});
|
|
|
|
threads_group.add(&threads_row);
|
|
threads_group.add(&error_row);
|
|
processing_page.add(&threads_group);
|
|
dialog.add(&processing_page);
|
|
|
|
// Accessibility page
|
|
let a11y_page = adw::PreferencesPage::builder()
|
|
.title("Accessibility")
|
|
.icon_name("preferences-desktop-accessibility-symbolic")
|
|
.build();
|
|
|
|
let a11y_group = adw::PreferencesGroup::builder()
|
|
.title("Visual Preferences")
|
|
.description("Override system settings for this app only")
|
|
.build();
|
|
|
|
let contrast_row = adw::SwitchRow::builder()
|
|
.title("High contrast")
|
|
.subtitle("Increase visual contrast throughout the app")
|
|
.active(config.high_contrast)
|
|
.build();
|
|
|
|
let large_text_row = adw::SwitchRow::builder()
|
|
.title("Large text")
|
|
.subtitle("Increase text size throughout the app")
|
|
.active(config.large_text)
|
|
.build();
|
|
|
|
let motion_row = adw::SwitchRow::builder()
|
|
.title("Reduced motion")
|
|
.subtitle("Minimize animations and transitions")
|
|
.active(config.reduced_motion)
|
|
.build();
|
|
|
|
a11y_group.add(&contrast_row);
|
|
a11y_group.add(&large_text_row);
|
|
a11y_group.add(&motion_row);
|
|
a11y_page.add(&a11y_group);
|
|
dialog.add(&a11y_page);
|
|
|
|
// Notifications page
|
|
let notify_page = adw::PreferencesPage::builder()
|
|
.title("Notifications")
|
|
.icon_name("preferences-system-notifications-symbolic")
|
|
.build();
|
|
|
|
let notify_group = adw::PreferencesGroup::builder()
|
|
.title("Completion")
|
|
.build();
|
|
|
|
let desktop_notify_row = adw::SwitchRow::builder()
|
|
.title("Desktop notification")
|
|
.subtitle("Show notification when processing completes")
|
|
.active(config.notify_on_completion)
|
|
.build();
|
|
|
|
let sound_row = adw::SwitchRow::builder()
|
|
.title("Completion sound")
|
|
.subtitle("Play a sound when processing completes")
|
|
.active(config.play_completion_sound)
|
|
.build();
|
|
|
|
let auto_open_row = adw::SwitchRow::builder()
|
|
.title("Auto-open output folder")
|
|
.subtitle("Open the output folder in file manager when done")
|
|
.active(config.auto_open_output)
|
|
.build();
|
|
|
|
notify_group.add(&desktop_notify_row);
|
|
notify_group.add(&sound_row);
|
|
notify_group.add(&auto_open_row);
|
|
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();
|
|
let overwrite = overwrite_row.clone();
|
|
let remember = remember_row.clone();
|
|
let skill = skill_row.clone();
|
|
let threads = threads_row.clone();
|
|
let error = error_row.clone();
|
|
let contrast = contrast_row.clone();
|
|
let large_text = large_text_row.clone();
|
|
let motion = motion_row.clone();
|
|
let notify = desktop_notify_row.clone();
|
|
let sound = sound_row.clone();
|
|
let auto_open = auto_open_row.clone();
|
|
let output_mode = output_mode_row.clone();
|
|
let fps_reset = fixed_path_state.clone();
|
|
reset_button.connect_clicked(move |_| {
|
|
let defaults = AppConfig::default();
|
|
subfolder.set_text(&defaults.output_subfolder);
|
|
overwrite.set_selected(0);
|
|
remember.set_active(defaults.remember_settings);
|
|
skill.set_selected(0);
|
|
threads.set_selected(0);
|
|
error.set_selected(0);
|
|
contrast.set_active(defaults.high_contrast);
|
|
large_text.set_active(defaults.large_text);
|
|
motion.set_active(defaults.reduced_motion);
|
|
notify.set_active(defaults.notify_on_completion);
|
|
sound.set_active(defaults.play_completion_sound);
|
|
auto_open.set_active(defaults.auto_open_output);
|
|
output_mode.set_selected(0);
|
|
*fps_reset.borrow_mut() = None;
|
|
});
|
|
}
|
|
|
|
// Preserve history settings from current config (not exposed in UI yet)
|
|
let hist_max_entries = config.history_max_entries;
|
|
let hist_max_days = config.history_max_days;
|
|
|
|
// Save settings when the dialog closes
|
|
dialog.connect_closed(move |_| {
|
|
let new_config = AppConfig {
|
|
first_run_complete: true,
|
|
tutorial_complete: true, // preserve if settings are being saved
|
|
output_subfolder: subfolder_row.text().to_string(),
|
|
output_fixed_path: if output_mode_row.selected() == 1 {
|
|
fixed_path_state.borrow().clone()
|
|
} else {
|
|
None
|
|
},
|
|
overwrite_behavior: match overwrite_row.selected() {
|
|
1 => OverwriteBehavior::AutoRename,
|
|
2 => OverwriteBehavior::Overwrite,
|
|
3 => OverwriteBehavior::Skip,
|
|
_ => OverwriteBehavior::Ask,
|
|
},
|
|
remember_settings: remember_row.is_active(),
|
|
skill_level: match skill_row.selected() {
|
|
1 => SkillLevel::Detailed,
|
|
_ => SkillLevel::Simple,
|
|
},
|
|
thread_count: match threads_row.selected() {
|
|
1 => pixstrip_core::config::ThreadCount::Manual(1),
|
|
2 => pixstrip_core::config::ThreadCount::Manual(2),
|
|
3 => pixstrip_core::config::ThreadCount::Manual(4),
|
|
4 => pixstrip_core::config::ThreadCount::Manual(8),
|
|
_ => pixstrip_core::config::ThreadCount::Auto,
|
|
},
|
|
error_behavior: match error_row.selected() {
|
|
1 => ErrorBehavior::PauseOnError,
|
|
_ => ErrorBehavior::SkipAndContinue,
|
|
},
|
|
notify_on_completion: desktop_notify_row.is_active(),
|
|
play_completion_sound: sound_row.is_active(),
|
|
auto_open_output: auto_open_row.is_active(),
|
|
high_contrast: contrast_row.is_active(),
|
|
large_text: large_text_row.is_active(),
|
|
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();
|
|
let _ = store.save(&new_config);
|
|
});
|
|
|
|
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
|
|
}
|