Add Escape key shortcut and subfolder prompt for folder drops
Escape now goes to previous wizard step (cancel/go back). When a folder with subfolders is dropped on the images step, an alert dialog asks whether to include subfolder images. The choice is remembered for the rest of the session.
This commit is contained in:
@@ -119,7 +119,7 @@ pub fn build_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.prev-step", &["<Alt>Left"]);
|
||||
app.set_accels_for_action("win.prev-step", &["<Alt>Left", "Escape"]);
|
||||
app.set_accels_for_action("win.process", &["<Control>Return"]);
|
||||
for i in 1..=9 {
|
||||
app.set_accels_for_action(
|
||||
|
||||
@@ -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::cell::RefCell<Option<bool>>> =
|
||||
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::<gtk::gio::File>()
|
||||
&& let Some(path) = file.path()
|
||||
{
|
||||
if path.is_dir() {
|
||||
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::<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;
|
||||
} 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<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<>k::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: >k::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(
|
||||
stack: >k::Stack,
|
||||
loaded_files: &std::rc::Rc<std::cell::RefCell<Vec<std::path::PathBuf>>>,
|
||||
|
||||
Reference in New Issue
Block a user