Fix drag-and-drop and file manager integration

Drag-and-drop from Nautilus on Wayland was broken by two issues:
- DropTarget only accepted COPY action, but Wayland compositor
  pre-selects MOVE, causing GTK4 to silently reject all drops
- Two competing DropTarget controllers on the same widget caused
  the gchararray target to match Nautilus formats first, swallowing
  the drop before the FileList target could receive it

Merged both drop targets into a single controller that tries
FileList first, then File, then falls back to URI text parsing.

File manager integration was broken by wrong command syntax
(non-existent --files flag) and wrong binary path resolution
inside AppImage. Split into separate GTK/CLI binary resolution
with APPIMAGE env var detection.
This commit is contained in:
2026-03-08 16:53:30 +02:00
parent 0eb7d24411
commit bcc53a0dc1
2 changed files with 124 additions and 109 deletions

View File

@@ -30,8 +30,10 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage {
let subfolder_choice: Rc<RefCell<Option<bool>>> = Rc::new(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()]);
// Accept both FileList (from file managers) and single File
let drop_target = gtk::DropTarget::new(gtk::gdk::FileList::static_type(), gtk::gdk::DragAction::COPY | gtk::gdk::DragAction::MOVE);
drop_target.set_types(&[gtk::gdk::FileList::static_type(), gtk::gio::File::static_type(), glib::GString::static_type()]);
drop_target.set_preload(true);
{
let loaded_files = state.loaded_files.clone();
@@ -40,40 +42,97 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage {
let stack_ref = stack.clone();
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()
{
// Collect paths from FileList, single File, or URI text
let mut paths: Vec<PathBuf> = Vec::new();
if let Ok(file_list) = value.get::<gtk::gdk::FileList>() {
for file in file_list.files() {
if let Some(path) = file.path() {
paths.push(path);
}
}
} else if let Ok(file) = value.get::<gtk::gio::File>() {
if let Some(path) = file.path() {
paths.push(path);
}
} else if let Ok(text) = value.get::<glib::GString>() {
// Handle URI text drops (from web browsers)
let text = text.trim().to_string();
let lower = text.to_lowercase();
let is_image_url = (lower.starts_with("http://") || lower.starts_with("https://"))
&& (lower.ends_with(".jpg")
|| lower.ends_with(".jpeg")
|| lower.ends_with(".png")
|| lower.ends_with(".webp")
|| lower.ends_with(".gif")
|| lower.ends_with(".avif")
|| lower.ends_with(".tiff")
|| lower.ends_with(".bmp")
|| lower.contains(".jpg?")
|| lower.contains(".jpeg?")
|| lower.contains(".png?")
|| lower.contains(".webp?"));
if is_image_url {
let loaded = loaded_files.clone();
let excl = excluded.clone();
let sz = sizes.clone();
let sr = stack_ref.clone();
let (tx, rx) = std::sync::mpsc::channel::<Option<std::path::PathBuf>>();
let url = text.clone();
std::thread::spawn(move || {
let result = download_image_url(&url);
let _ = tx.send(result);
});
glib::timeout_add_local(std::time::Duration::from_millis(100), move || {
match rx.try_recv() {
Ok(Some(path)) => {
let mut files = loaded.borrow_mut();
if !files.contains(&path) {
files.push(path);
}
let count = files.len();
drop(files);
refresh_grid(&sr, &loaded, &excl, &sz, count);
glib::ControlFlow::Break
}
Ok(None) => glib::ControlFlow::Break,
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
Err(_) => glib::ControlFlow::Break,
}
});
return true;
}
return false;
}
if paths.is_empty() {
return false;
}
for path in paths {
if path.is_dir() {
let has_subdirs = has_subfolders(&path);
if !has_subdirs {
let mut files = loaded_files.borrow_mut();
add_images_flat(&path, &mut files);
let count = files.len();
drop(files);
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
} else {
let choice = *subfolder_choice.borrow();
match choice {
Some(true) => {
let mut files = loaded_files.borrow_mut();
add_images_from_dir(&path, &mut files);
let count = files.len();
drop(files);
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
}
Some(false) => {
let mut files = loaded_files.borrow_mut();
add_images_flat(&path, &mut files);
let count = files.len();
drop(files);
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
}
None => {
let mut files = loaded_files.borrow_mut();
add_images_flat(&path, &mut files);
let count = files.len();
drop(files);
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
let loaded_files = loaded_files.clone();
let excluded = excluded.clone();
@@ -98,88 +157,23 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage {
}
}
}
return true;
} else if is_image_file(&path) {
let mut files = loaded_files.borrow_mut();
if !files.contains(&path) {
files.push(path);
}
let count = files.len();
drop(files);
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
return true;
}
}
false
let count = loaded_files.borrow().len();
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
true
});
}
stack.add_controller(drop_target);
// Also accept URI text drops (from web browsers)
let uri_drop = gtk::DropTarget::new(glib::GString::static_type(), gtk::gdk::DragAction::COPY);
{
let loaded_files = state.loaded_files.clone();
let excluded = state.excluded_files.clone();
let sizes = state.file_sizes.clone();
let stack_ref = stack.clone();
uri_drop.connect_drop(move |_target, value, _x, _y| {
if let Ok(text) = value.get::<glib::GString>() {
let text = text.trim().to_string();
// Check if it looks like an image URL
let lower = text.to_lowercase();
let is_image_url = (lower.starts_with("http://") || lower.starts_with("https://"))
&& (lower.ends_with(".jpg")
|| lower.ends_with(".jpeg")
|| lower.ends_with(".png")
|| lower.ends_with(".webp")
|| lower.ends_with(".gif")
|| lower.ends_with(".avif")
|| lower.ends_with(".tiff")
|| lower.ends_with(".bmp")
|| lower.contains(".jpg?")
|| lower.contains(".jpeg?")
|| lower.contains(".png?")
|| lower.contains(".webp?"));
if is_image_url {
let loaded = loaded_files.clone();
let excl = excluded.clone();
let sz = sizes.clone();
let sr = stack_ref.clone();
// Download in background thread
let (tx, rx) = std::sync::mpsc::channel::<Option<std::path::PathBuf>>();
let url = text.clone();
std::thread::spawn(move || {
let result = download_image_url(&url);
let _ = tx.send(result);
});
glib::timeout_add_local(std::time::Duration::from_millis(100), move || {
match rx.try_recv() {
Ok(Some(path)) => {
let mut files = loaded.borrow_mut();
if !files.contains(&path) {
files.push(path);
}
let count = files.len();
drop(files);
refresh_grid(&sr, &loaded, &excl, &sz, count);
glib::ControlFlow::Break
}
Ok(None) => glib::ControlFlow::Break,
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
Err(_) => glib::ControlFlow::Break,
}
});
return true;
}
}
false
});
}
stack.add_controller(uri_drop);
adw::NavigationPage::builder()
.title("Add Images")
.tag("step-images")