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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user