diff --git a/pixstrip-gtk/src/app.rs b/pixstrip-gtk/src/app.rs index d63d59f..31823c1 100644 --- a/pixstrip-gtk/src/app.rs +++ b/pixstrip-gtk/src/app.rs @@ -119,7 +119,7 @@ pub fn build_app() -> adw::Application { fn setup_shortcuts(app: &adw::Application) { app.set_accels_for_action("win.next-step", &["Right"]); - app.set_accels_for_action("win.prev-step", &["Left"]); + app.set_accels_for_action("win.prev-step", &["Left", "Escape"]); app.set_accels_for_action("win.process", &["Return"]); for i in 1..=9 { app.set_accels_for_action( diff --git a/pixstrip-gtk/src/steps/step_images.rs b/pixstrip-gtk/src/steps/step_images.rs index cbde08d..d4acfee 100644 --- a/pixstrip-gtk/src/steps/step_images.rs +++ b/pixstrip-gtk/src/steps/step_images.rs @@ -16,6 +16,10 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage { stack.set_visible_child_name("empty"); + // Session-level remembered subfolder choice (None = not yet asked) + let subfolder_choice: std::rc::Rc>> = + std::rc::Rc::new(std::cell::RefCell::new(None)); + // Set up drag-and-drop on the entire page let drop_target = gtk::DropTarget::new(gtk::gio::File::static_type(), gtk::gdk::DragAction::COPY); drop_target.set_types(&[gtk::gio::File::static_type()]); @@ -24,16 +28,69 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage { let loaded_files = state.loaded_files.clone(); let excluded = state.excluded_files.clone(); let stack_ref = stack.clone(); - drop_target.connect_drop(move |_target, value, _x, _y| { + let subfolder_choice = subfolder_choice.clone(); + drop_target.connect_drop(move |target, value, _x, _y| { if let Ok(file) = value.get::() && let Some(path) = file.path() { if path.is_dir() { - let mut files = loaded_files.borrow_mut(); - add_images_from_dir(&path, &mut files); - let count = files.len(); - drop(files); - update_loaded_ui(&stack_ref, &loaded_files, &excluded, count); + let has_subdirs = has_subfolders(&path); + if !has_subdirs { + // No subfolders - just load top-level images + let mut files = loaded_files.borrow_mut(); + add_images_flat(&path, &mut files); + let count = files.len(); + drop(files); + update_loaded_ui(&stack_ref, &loaded_files, &excluded, count); + } else { + let choice = *subfolder_choice.borrow(); + match choice { + Some(true) => { + // Remembered: include subfolders + let mut files = loaded_files.borrow_mut(); + add_images_from_dir(&path, &mut files); + let count = files.len(); + drop(files); + update_loaded_ui(&stack_ref, &loaded_files, &excluded, count); + } + Some(false) => { + // Remembered: top-level only + let mut files = loaded_files.borrow_mut(); + add_images_flat(&path, &mut files); + let count = files.len(); + drop(files); + update_loaded_ui(&stack_ref, &loaded_files, &excluded, count); + } + None => { + // Not yet asked - add top-level now, then prompt + let mut files = loaded_files.borrow_mut(); + add_images_flat(&path, &mut files); + let count = files.len(); + drop(files); + update_loaded_ui(&stack_ref, &loaded_files, &excluded, count); + + // Show dialog asynchronously + let loaded_files = loaded_files.clone(); + let excluded = excluded.clone(); + let stack_ref = stack_ref.clone(); + let subfolder_choice = subfolder_choice.clone(); + let dir_path = path.clone(); + let window = target.widget() + .and_then(|w| w.root()) + .and_then(|r| r.downcast::().ok()); + gtk::glib::idle_add_local_once(move || { + show_subfolder_prompt( + window.as_ref(), + &dir_path, + &loaded_files, + &excluded, + &stack_ref, + &subfolder_choice, + ); + }); + } + } + } return true; } else if is_image_file(&path) { let mut files = loaded_files.borrow_mut(); @@ -79,6 +136,81 @@ fn add_images_from_dir(dir: &std::path::Path, files: &mut Vec) { + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() && is_image_file(&path) && !files.contains(&path) { + files.push(path); + } + } + } +} + +/// Check if a directory contains any subdirectories +fn has_subfolders(dir: &std::path::Path) -> bool { + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.flatten() { + if entry.path().is_dir() { + return true; + } + } + } + false +} + +/// Add only the images from subfolders (not top-level, since those were already added) +fn add_images_from_subdirs(dir: &std::path::Path, files: &mut Vec) { + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + add_images_from_dir(&path, files); + } + } + } +} + +fn show_subfolder_prompt( + window: Option<>k::Window>, + dir: &std::path::Path, + loaded_files: &std::rc::Rc>>, + excluded: &std::rc::Rc>>, + stack: >k::Stack, + subfolder_choice: &std::rc::Rc>>, +) { + let dialog = adw::AlertDialog::builder() + .heading("Include subfolders?") + .body("This folder contains subfolders. Would you like to include images from subfolders too?") + .build(); + dialog.add_response("no", "Top-level Only"); + dialog.add_response("yes", "Include Subfolders"); + dialog.set_default_response(Some("yes")); + dialog.set_response_appearance("yes", adw::ResponseAppearance::Suggested); + + let loaded_files = loaded_files.clone(); + let excluded = excluded.clone(); + let stack = stack.clone(); + let subfolder_choice = subfolder_choice.clone(); + let dir = dir.to_path_buf(); + dialog.connect_response(None, move |_dialog, response| { + let include_subdirs = response == "yes"; + *subfolder_choice.borrow_mut() = Some(include_subdirs); + if include_subdirs { + let mut files = loaded_files.borrow_mut(); + add_images_from_subdirs(&dir, &mut files); + let count = files.len(); + drop(files); + update_loaded_ui(&stack, &loaded_files, &excluded, count); + } + }); + + if let Some(win) = window { + dialog.present(Some(win)); + } +} + fn update_loaded_ui( stack: >k::Stack, loaded_files: &std::rc::Rc>>,