diff --git a/pixstrip-core/src/fm_integration.rs b/pixstrip-core/src/fm_integration.rs index 2956057..86f12b6 100644 --- a/pixstrip-core/src/fm_integration.rs +++ b/pixstrip-core/src/fm_integration.rs @@ -84,15 +84,14 @@ fn shell_safe(s: &str) -> String { .collect() } -fn pixstrip_bin() -> String { - // Try to find the pixstrip binary path +/// Path to the GTK binary for "Open in Pixstrip" actions. +fn pixstrip_gtk_bin() -> String { + // When running from AppImage, use the AppImage path directly + if let Ok(appimage) = std::env::var("APPIMAGE") { + return appimage; + } if let Ok(exe) = std::env::current_exe() { - // If running from the GTK app, find the CLI sibling let dir = exe.parent().unwrap_or(Path::new("/usr/bin")); - let cli_path = dir.join("pixstrip"); - if cli_path.exists() { - return cli_path.display().to_string(); - } let gtk_path = dir.join("pixstrip-gtk"); if gtk_path.exists() { return gtk_path.display().to_string(); @@ -102,6 +101,22 @@ fn pixstrip_bin() -> String { "pixstrip-gtk".into() } +/// Path to the CLI binary for preset processing actions. +fn pixstrip_cli_bin() -> String { + // When running from AppImage, use the AppImage path directly + if let Ok(appimage) = std::env::var("APPIMAGE") { + return appimage; + } + if let Ok(exe) = std::env::current_exe() { + let dir = exe.parent().unwrap_or(Path::new("/usr/bin")); + let cli_path = dir.join("pixstrip"); + if cli_path.exists() { + return cli_path.display().to_string(); + } + } + "pixstrip".into() +} + fn get_preset_names() -> Vec { let mut names: Vec = Preset::all_builtins() .into_iter() @@ -138,7 +153,8 @@ fn install_nautilus() -> Result<()> { let dir = nautilus_extension_dir(); std::fs::create_dir_all(&dir)?; - let bin = pixstrip_bin(); + let gtk_bin = pixstrip_gtk_bin(); + let cli_bin = pixstrip_cli_bin(); let presets = get_preset_names(); let mut preset_items = String::new(); @@ -156,7 +172,8 @@ fn install_nautilus() -> Result<()> { )); } - let escaped_bin = bin.replace('\\', "\\\\").replace('\'', "\\'"); + let escaped_gtk_bin = gtk_bin.replace('\\', "\\\\").replace('\'', "\\'"); + let escaped_cli_bin = cli_bin.replace('\\', "\\\\").replace('\'', "\\'"); let script = format!( r#"import subprocess from gi.repository import Nautilus, GObject @@ -204,14 +221,15 @@ class PixstripExtension(GObject.GObject, Nautilus.MenuProvider): def _on_open(self, menu, files): paths = [f.get_location().get_path() for f in files if f.get_location()] - subprocess.Popen(['{bin}', '--files'] + paths) + subprocess.Popen(['{gtk_bin}'] + paths) def _on_preset(self, menu, preset_name, files): paths = [f.get_location().get_path() for f in files if f.get_location()] - subprocess.Popen(['{bin}', '--preset', preset_name, '--files'] + paths) + subprocess.Popen(['{cli_bin}', 'process', '--preset', preset_name] + paths) "#, preset_items = preset_items, - bin = escaped_bin, + gtk_bin = escaped_gtk_bin, + cli_bin = escaped_cli_bin, ); atomic_write(&nautilus_extension_path(), &script)?; @@ -245,19 +263,20 @@ fn install_nemo() -> Result<()> { let dir = nemo_action_dir(); std::fs::create_dir_all(&dir)?; - let bin = pixstrip_bin(); + let gtk_bin = pixstrip_gtk_bin(); + let cli_bin = pixstrip_cli_bin(); // Main "Open in Pixstrip" action let open_action = format!( "[Nemo Action]\n\ Name=Open in Pixstrip...\n\ Comment=Process images with Pixstrip\n\ - Exec={bin} --files %F\n\ + Exec={gtk_bin} %F\n\ Icon-Name=applications-graphics-symbolic\n\ Selection=Any\n\ Extensions=jpg;jpeg;png;webp;gif;tiff;tif;avif;bmp;\n\ Mimetypes=image/*;\n", - bin = bin, + gtk_bin = gtk_bin, ); atomic_write(&nemo_action_path(), &open_action)?; @@ -270,14 +289,14 @@ fn install_nemo() -> Result<()> { "[Nemo Action]\n\ Name=Pixstrip: {name}\n\ Comment=Process with {name} preset\n\ - Exec={bin} --preset \"{safe_label}\" --files %F\n\ + Exec={cli_bin} process --preset \"{safe_label}\" %F\n\ Icon-Name=applications-graphics-symbolic\n\ Selection=Any\n\ Extensions=jpg;jpeg;png;webp;gif;tiff;tif;avif;bmp;\n\ Mimetypes=image/*;\n", name = name, safe_label = shell_safe(name), - bin = bin, + cli_bin = cli_bin, ); atomic_write(&action_path, &action)?; } @@ -324,7 +343,8 @@ fn install_thunar() -> Result<()> { let dir = thunar_action_dir(); std::fs::create_dir_all(&dir)?; - let bin = pixstrip_bin(); + let gtk_bin = pixstrip_gtk_bin(); + let cli_bin = pixstrip_cli_bin(); let presets = get_preset_names(); let mut actions = String::from("\n\n"); @@ -334,13 +354,13 @@ fn install_thunar() -> Result<()> { " \n\ \x20 applications-graphics-symbolic\n\ \x20 Open in Pixstrip...\n\ - \x20 {bin} --files %F\n\ + \x20 {gtk_bin} %F\n\ \x20 Process images with Pixstrip\n\ \x20 *.jpg;*.jpeg;*.png;*.webp;*.gif;*.tiff;*.tif;*.avif;*.bmp\n\ \x20 \n\ \x20 \n\ \n", - bin = bin, + gtk_bin = gtk_bin, )); for name in &presets { @@ -348,7 +368,7 @@ fn install_thunar() -> Result<()> { " \n\ \x20 applications-graphics-symbolic\n\ \x20 Pixstrip: {xml_name}\n\ - \x20 {bin} --preset \"{safe_label}\" --files %F\n\ + \x20 {cli_bin} process --preset \"{safe_label}\" %F\n\ \x20 Process with {xml_name} preset\n\ \x20 *.jpg;*.jpeg;*.png;*.webp;*.gif;*.tiff;*.tif;*.avif;*.bmp\n\ \x20 \n\ @@ -356,7 +376,7 @@ fn install_thunar() -> Result<()> { \n", xml_name = name.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """), safe_label = shell_safe(name), - bin = bin, + cli_bin = cli_bin, )); } @@ -392,7 +412,8 @@ fn install_dolphin() -> Result<()> { let dir = dolphin_service_dir(); std::fs::create_dir_all(&dir)?; - let bin = pixstrip_bin(); + let gtk_bin = pixstrip_gtk_bin(); + let cli_bin = pixstrip_cli_bin(); let presets = get_preset_names(); let mut desktop = format!( @@ -406,8 +427,8 @@ fn install_dolphin() -> Result<()> { [Desktop Action Open]\n\ Name=Open in Pixstrip...\n\ Icon=applications-graphics-symbolic\n\ - Exec={bin} --files %F\n\n", - bin = bin, + Exec={gtk_bin} %F\n\n", + gtk_bin = gtk_bin, preset_actions = presets .iter() .enumerate() @@ -421,11 +442,11 @@ fn install_dolphin() -> Result<()> { "[Desktop Action Preset{i}]\n\ Name={name}\n\ Icon=applications-graphics-symbolic\n\ - Exec={bin} --preset \"{safe_label}\" --files %F\n\n", + Exec={cli_bin} process --preset \"{safe_label}\" %F\n\n", i = i, name = name, safe_label = shell_safe(name), - bin = bin, + cli_bin = cli_bin, )); } diff --git a/pixstrip-gtk/src/steps/step_images.rs b/pixstrip-gtk/src/steps/step_images.rs index 8faf466..c6564e4 100644 --- a/pixstrip-gtk/src/steps/step_images.rs +++ b/pixstrip-gtk/src/steps/step_images.rs @@ -30,8 +30,10 @@ pub fn build_images_page(state: &AppState) -> adw::NavigationPage { let subfolder_choice: Rc>> = 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::() - && let Some(path) = file.path() - { + // Collect paths from FileList, single File, or URI text + let mut paths: Vec = Vec::new(); + if let Ok(file_list) = value.get::() { + for file in file_list.files() { + if let Some(path) = file.path() { + paths.push(path); + } + } + } else if let Ok(file) = value.get::() { + if let Some(path) = file.path() { + paths.push(path); + } + } else if let Ok(text) = value.get::() { + // 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::>(); + 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::() { - 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::>(); - 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")