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.
This commit is contained in:
@@ -112,6 +112,68 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
|
|
||||||
stack.add_controller(drop_target);
|
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::<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 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, 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()
|
adw::NavigationPage::builder()
|
||||||
.title("Add Images")
|
.title("Add Images")
|
||||||
.tag("step-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<std::path::PathBuf> {
|
||||||
|
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
|
/// Set all CheckButton widgets within a container to a given state
|
||||||
pub fn set_all_checkboxes_in(widget: >k::Widget, active: bool) {
|
pub fn set_all_checkboxes_in(widget: >k::Widget, active: bool) {
|
||||||
if let Some(check) = widget.downcast_ref::<gtk::CheckButton>() {
|
if let Some(check) = widget.downcast_ref::<gtk::CheckButton>() {
|
||||||
|
|||||||
Reference in New Issue
Block a user