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(); let subfolder_row = adw::EntryRow::builder() .title("Default output subfolder") .text(&config.output_subfolder) .build(); 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(&subfolder_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(); let file_managers = [ ("Nautilus", "org.gnome.Nautilus"), ("Nemo", "org.nemo.Nemo"), ("Thunar", "thunar"), ("Dolphin", "org.kde.dolphin"), ]; let mut found_fm = false; for (name, 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 row = adw::SwitchRow::builder() .title(*name) .subtitle(format!("Add right-click menu to {}", name)) .active(false) .build(); 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); // 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(); 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); }); } // 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, output_subfolder: subfolder_row.text().to_string(), output_fixed_path: 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, }; let store = ConfigStore::new(); let _ = store.save(&new_config); }); dialog }