From 45aaa02f19bc456f1ba4bb9f76dacbb7824de159 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 16:02:39 +0200 Subject: [PATCH] Add URL drag-and-drop support for images from web browsers Users can now drag image URLs from web browsers into the image step. URLs ending in common image extensions are downloaded to a temp directory and added to the batch. Uses GIO for the download in a background thread. --- pixstrip-gtk/src/steps/step_images.rs | 105 ++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/pixstrip-gtk/src/steps/step_images.rs b/pixstrip-gtk/src/steps/step_images.rs index 2609a86..0cec33d 100644 --- a/pixstrip-gtk/src/steps/step_images.rs +++ b/pixstrip-gtk/src/steps/step_images.rs @@ -112,6 +112,68 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage { 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 stack_ref = stack.clone(); + uri_drop.connect_drop(move |_target, value, _x, _y| { + if let Ok(text) = value.get::() { + 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 sr = stack_ref.clone(); + // Download in background thread + let (tx, rx) = std::sync::mpsc::channel::>(); + 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, 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") @@ -801,6 +863,49 @@ fn load_thumbnail(path: &std::path::Path, picture: >k::Picture, stack: >k::S } } +/// Download an image from a URL to a temporary file +fn download_image_url(url: &str) -> Option { + let temp_dir = std::env::temp_dir().join("pixstrip-downloads"); + std::fs::create_dir_all(&temp_dir).ok()?; + + // Extract filename from URL + let url_path = url.split('?').next().unwrap_or(url); + let filename = url_path + .rsplit('/') + .next() + .unwrap_or("downloaded.jpg") + .to_string(); + + let dest = temp_dir.join(&filename); + + // Use GIO for the download (synchronous, runs in background thread) + let gfile = gtk::gio::File::for_uri(url); + let stream = gfile.read(gtk::gio::Cancellable::NONE).ok()?; + + let mut buf = Vec::new(); + loop { + let bytes = stream.read_bytes(8192, gtk::gio::Cancellable::NONE).ok()?; + if bytes.is_empty() { + break; + } + buf.extend_from_slice(&bytes); + } + + if buf.is_empty() { + return None; + } + + std::fs::write(&dest, &buf).ok()?; + + // Verify it's actually an image + if is_image_file(&dest) { + Some(dest) + } else { + let _ = std::fs::remove_file(&dest); + None + } +} + /// Set all CheckButton widgets within a container to a given state pub fn set_all_checkboxes_in(widget: >k::Widget, active: bool) { if let Some(check) = widget.downcast_ref::() {