Add Escape key shortcut and subfolder prompt for folder drops

This commit is contained in:
2026-03-06 13:45:06 +02:00
parent 5428214d6c
commit ea596e01fe
2 changed files with 139 additions and 7 deletions

View File

@@ -119,7 +119,7 @@ pub fn build_app() -> adw::Application {
fn setup_shortcuts(app: &adw::Application) { fn setup_shortcuts(app: &adw::Application) {
app.set_accels_for_action("win.next-step", &["<Alt>Right"]); app.set_accels_for_action("win.next-step", &["<Alt>Right"]);
app.set_accels_for_action("win.prev-step", &["<Alt>Left"]); app.set_accels_for_action("win.prev-step", &["<Alt>Left", "Escape"]);
app.set_accels_for_action("win.process", &["<Control>Return"]); app.set_accels_for_action("win.process", &["<Control>Return"]);
for i in 1..=9 { for i in 1..=9 {
app.set_accels_for_action( app.set_accels_for_action(

View File

@@ -16,6 +16,10 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage {
stack.set_visible_child_name("empty"); stack.set_visible_child_name("empty");
// Session-level remembered subfolder choice (None = not yet asked)
let subfolder_choice: std::rc::Rc<std::cell::RefCell<Option<bool>>> =
std::rc::Rc::new(std::cell::RefCell::new(None));
// Set up drag-and-drop on the entire page // Set up drag-and-drop on the entire page
let drop_target = gtk::DropTarget::new(gtk::gio::File::static_type(), gtk::gdk::DragAction::COPY); let drop_target = gtk::DropTarget::new(gtk::gio::File::static_type(), gtk::gdk::DragAction::COPY);
drop_target.set_types(&[gtk::gio::File::static_type()]); 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 loaded_files = state.loaded_files.clone();
let excluded = state.excluded_files.clone(); let excluded = state.excluded_files.clone();
let stack_ref = stack.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::<gtk::gio::File>() if let Ok(file) = value.get::<gtk::gio::File>()
&& let Some(path) = file.path() && let Some(path) = file.path()
{ {
if path.is_dir() { if path.is_dir() {
let mut files = loaded_files.borrow_mut(); let has_subdirs = has_subfolders(&path);
add_images_from_dir(&path, &mut files); if !has_subdirs {
let count = files.len(); // No subfolders - just load top-level images
drop(files); let mut files = loaded_files.borrow_mut();
update_loaded_ui(&stack_ref, &loaded_files, &excluded, count); 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::<gtk::Window>().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; return true;
} else if is_image_file(&path) { } else if is_image_file(&path) {
let mut files = loaded_files.borrow_mut(); let mut files = loaded_files.borrow_mut();
@@ -79,6 +136,81 @@ fn add_images_from_dir(dir: &std::path::Path, files: &mut Vec<std::path::PathBuf
} }
} }
/// Add only top-level images from a directory (no recursion into subfolders)
fn add_images_flat(dir: &std::path::Path, files: &mut Vec<std::path::PathBuf>) {
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<std::path::PathBuf>) {
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<&gtk::Window>,
dir: &std::path::Path,
loaded_files: &std::rc::Rc<std::cell::RefCell<Vec<std::path::PathBuf>>>,
excluded: &std::rc::Rc<std::cell::RefCell<std::collections::HashSet<std::path::PathBuf>>>,
stack: &gtk::Stack,
subfolder_choice: &std::rc::Rc<std::cell::RefCell<Option<bool>>>,
) {
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( fn update_loaded_ui(
stack: &gtk::Stack, stack: &gtk::Stack,
loaded_files: &std::rc::Rc<std::cell::RefCell<Vec<std::path::PathBuf>>>, loaded_files: &std::rc::Rc<std::cell::RefCell<Vec<std::path::PathBuf>>>,