Add Escape key shortcut and subfolder prompt for folder drops
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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<>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(
|
fn update_loaded_ui(
|
||||||
stack: >k::Stack,
|
stack: >k::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>>>,
|
||||||
|
|||||||
Reference in New Issue
Block a user