Files
pixstrip/pixstrip-gtk/src/settings.rs

322 lines
11 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();
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(&gtk::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(&notify_group);
dialog.add(&notify_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
}