Add fixed output folder option in settings

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.
This commit is contained in:
2026-03-06 17:23:57 +02:00
parent fbb9cddbb8
commit 0460763d42

View File

@@ -20,11 +20,84 @@ pub fn build_settings_dialog() -> adw::PreferencesDialog {
.title("Output") .title("Output")
.build(); .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() let subfolder_row = adw::EntryRow::builder()
.title("Default output subfolder") .title("Default output subfolder")
.text(&config.output_subfolder) .text(&config.output_subfolder)
.visible(config.output_fixed_path.is_none())
.build(); .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(&gtk::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() let overwrite_row = adw::ComboRow::builder()
.title("Default overwrite behavior") .title("Default overwrite behavior")
.subtitle("What to do when output files already exist") .subtitle("What to do when output files already exist")
@@ -49,7 +122,9 @@ pub fn build_settings_dialog() -> adw::PreferencesDialog {
.active(config.remember_settings) .active(config.remember_settings)
.build(); .build();
output_group.add(&output_mode_row);
output_group.add(&subfolder_row); output_group.add(&subfolder_row);
output_group.add(&fixed_path_row);
output_group.add(&overwrite_row); output_group.add(&overwrite_row);
output_group.add(&remember_row); output_group.add(&remember_row);
general_page.add(&output_group); general_page.add(&output_group);
@@ -366,6 +441,8 @@ pub fn build_settings_dialog() -> adw::PreferencesDialog {
let notify = desktop_notify_row.clone(); let notify = desktop_notify_row.clone();
let sound = sound_row.clone(); let sound = sound_row.clone();
let auto_open = auto_open_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 |_| { reset_button.connect_clicked(move |_| {
let defaults = AppConfig::default(); let defaults = AppConfig::default();
subfolder.set_text(&defaults.output_subfolder); subfolder.set_text(&defaults.output_subfolder);
@@ -380,6 +457,8 @@ pub fn build_settings_dialog() -> adw::PreferencesDialog {
notify.set_active(defaults.notify_on_completion); notify.set_active(defaults.notify_on_completion);
sound.set_active(defaults.play_completion_sound); sound.set_active(defaults.play_completion_sound);
auto_open.set_active(defaults.auto_open_output); auto_open.set_active(defaults.auto_open_output);
output_mode.set_selected(0);
*fps_reset.borrow_mut() = None;
}); });
} }
@@ -393,7 +472,11 @@ pub fn build_settings_dialog() -> adw::PreferencesDialog {
first_run_complete: true, first_run_complete: true,
tutorial_complete: true, // preserve if settings are being saved tutorial_complete: true, // preserve if settings are being saved
output_subfolder: subfolder_row.text().to_string(), output_subfolder: subfolder_row.text().to_string(),
output_fixed_path: None, output_fixed_path: if output_mode_row.selected() == 1 {
fixed_path_state.borrow().clone()
} else {
None
},
overwrite_behavior: match overwrite_row.selected() { overwrite_behavior: match overwrite_row.selected() {
1 => OverwriteBehavior::AutoRename, 1 => OverwriteBehavior::AutoRename,
2 => OverwriteBehavior::Overwrite, 2 => OverwriteBehavior::Overwrite,