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:
@@ -84,15 +84,14 @@ fn shell_safe(s: &str) -> String {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pixstrip_bin() -> String {
|
/// Path to the GTK binary for "Open in Pixstrip" actions.
|
||||||
// Try to find the pixstrip binary path
|
fn pixstrip_gtk_bin() -> String {
|
||||||
if let Ok(exe) = std::env::current_exe() {
|
// When running from AppImage, use the AppImage path directly
|
||||||
// If running from the GTK app, find the CLI sibling
|
if let Ok(appimage) = std::env::var("APPIMAGE") {
|
||||||
let dir = exe.parent().unwrap_or(Path::new("/usr/bin"));
|
return appimage;
|
||||||
let cli_path = dir.join("pixstrip");
|
|
||||||
if cli_path.exists() {
|
|
||||||
return cli_path.display().to_string();
|
|
||||||
}
|
}
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
let dir = exe.parent().unwrap_or(Path::new("/usr/bin"));
|
||||||
let gtk_path = dir.join("pixstrip-gtk");
|
let gtk_path = dir.join("pixstrip-gtk");
|
||||||
if gtk_path.exists() {
|
if gtk_path.exists() {
|
||||||
return gtk_path.display().to_string();
|
return gtk_path.display().to_string();
|
||||||
@@ -102,6 +101,22 @@ fn pixstrip_bin() -> String {
|
|||||||
"pixstrip-gtk".into()
|
"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<String> {
|
fn get_preset_names() -> Vec<String> {
|
||||||
let mut names: Vec<String> = Preset::all_builtins()
|
let mut names: Vec<String> = Preset::all_builtins()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -138,7 +153,8 @@ fn install_nautilus() -> Result<()> {
|
|||||||
let dir = nautilus_extension_dir();
|
let dir = nautilus_extension_dir();
|
||||||
std::fs::create_dir_all(&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 presets = get_preset_names();
|
||||||
|
|
||||||
let mut preset_items = String::new();
|
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!(
|
let script = format!(
|
||||||
r#"import subprocess
|
r#"import subprocess
|
||||||
from gi.repository import Nautilus, GObject
|
from gi.repository import Nautilus, GObject
|
||||||
@@ -204,14 +221,15 @@ class PixstripExtension(GObject.GObject, Nautilus.MenuProvider):
|
|||||||
|
|
||||||
def _on_open(self, menu, files):
|
def _on_open(self, menu, files):
|
||||||
paths = [f.get_location().get_path() for f in files if f.get_location()]
|
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):
|
def _on_preset(self, menu, preset_name, files):
|
||||||
paths = [f.get_location().get_path() for f in files if f.get_location()]
|
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,
|
preset_items = preset_items,
|
||||||
bin = escaped_bin,
|
gtk_bin = escaped_gtk_bin,
|
||||||
|
cli_bin = escaped_cli_bin,
|
||||||
);
|
);
|
||||||
|
|
||||||
atomic_write(&nautilus_extension_path(), &script)?;
|
atomic_write(&nautilus_extension_path(), &script)?;
|
||||||
@@ -245,19 +263,20 @@ fn install_nemo() -> Result<()> {
|
|||||||
let dir = nemo_action_dir();
|
let dir = nemo_action_dir();
|
||||||
std::fs::create_dir_all(&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
|
// Main "Open in Pixstrip" action
|
||||||
let open_action = format!(
|
let open_action = format!(
|
||||||
"[Nemo Action]\n\
|
"[Nemo Action]\n\
|
||||||
Name=Open in Pixstrip...\n\
|
Name=Open in Pixstrip...\n\
|
||||||
Comment=Process images with Pixstrip\n\
|
Comment=Process images with Pixstrip\n\
|
||||||
Exec={bin} --files %F\n\
|
Exec={gtk_bin} %F\n\
|
||||||
Icon-Name=applications-graphics-symbolic\n\
|
Icon-Name=applications-graphics-symbolic\n\
|
||||||
Selection=Any\n\
|
Selection=Any\n\
|
||||||
Extensions=jpg;jpeg;png;webp;gif;tiff;tif;avif;bmp;\n\
|
Extensions=jpg;jpeg;png;webp;gif;tiff;tif;avif;bmp;\n\
|
||||||
Mimetypes=image/*;\n",
|
Mimetypes=image/*;\n",
|
||||||
bin = bin,
|
gtk_bin = gtk_bin,
|
||||||
);
|
);
|
||||||
atomic_write(&nemo_action_path(), &open_action)?;
|
atomic_write(&nemo_action_path(), &open_action)?;
|
||||||
|
|
||||||
@@ -270,14 +289,14 @@ fn install_nemo() -> Result<()> {
|
|||||||
"[Nemo Action]\n\
|
"[Nemo Action]\n\
|
||||||
Name=Pixstrip: {name}\n\
|
Name=Pixstrip: {name}\n\
|
||||||
Comment=Process with {name} preset\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\
|
Icon-Name=applications-graphics-symbolic\n\
|
||||||
Selection=Any\n\
|
Selection=Any\n\
|
||||||
Extensions=jpg;jpeg;png;webp;gif;tiff;tif;avif;bmp;\n\
|
Extensions=jpg;jpeg;png;webp;gif;tiff;tif;avif;bmp;\n\
|
||||||
Mimetypes=image/*;\n",
|
Mimetypes=image/*;\n",
|
||||||
name = name,
|
name = name,
|
||||||
safe_label = shell_safe(name),
|
safe_label = shell_safe(name),
|
||||||
bin = bin,
|
cli_bin = cli_bin,
|
||||||
);
|
);
|
||||||
atomic_write(&action_path, &action)?;
|
atomic_write(&action_path, &action)?;
|
||||||
}
|
}
|
||||||
@@ -324,7 +343,8 @@ fn install_thunar() -> Result<()> {
|
|||||||
let dir = thunar_action_dir();
|
let dir = thunar_action_dir();
|
||||||
std::fs::create_dir_all(&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 presets = get_preset_names();
|
||||||
|
|
||||||
let mut actions = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<actions>\n");
|
let mut actions = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<actions>\n");
|
||||||
@@ -334,13 +354,13 @@ fn install_thunar() -> Result<()> {
|
|||||||
" <action>\n\
|
" <action>\n\
|
||||||
\x20 <icon>applications-graphics-symbolic</icon>\n\
|
\x20 <icon>applications-graphics-symbolic</icon>\n\
|
||||||
\x20 <name>Open in Pixstrip...</name>\n\
|
\x20 <name>Open in Pixstrip...</name>\n\
|
||||||
\x20 <command>{bin} --files %F</command>\n\
|
\x20 <command>{gtk_bin} %F</command>\n\
|
||||||
\x20 <description>Process images with Pixstrip</description>\n\
|
\x20 <description>Process images with Pixstrip</description>\n\
|
||||||
\x20 <patterns>*.jpg;*.jpeg;*.png;*.webp;*.gif;*.tiff;*.tif;*.avif;*.bmp</patterns>\n\
|
\x20 <patterns>*.jpg;*.jpeg;*.png;*.webp;*.gif;*.tiff;*.tif;*.avif;*.bmp</patterns>\n\
|
||||||
\x20 <image-files/>\n\
|
\x20 <image-files/>\n\
|
||||||
\x20 <directories/>\n\
|
\x20 <directories/>\n\
|
||||||
</action>\n",
|
</action>\n",
|
||||||
bin = bin,
|
gtk_bin = gtk_bin,
|
||||||
));
|
));
|
||||||
|
|
||||||
for name in &presets {
|
for name in &presets {
|
||||||
@@ -348,7 +368,7 @@ fn install_thunar() -> Result<()> {
|
|||||||
" <action>\n\
|
" <action>\n\
|
||||||
\x20 <icon>applications-graphics-symbolic</icon>\n\
|
\x20 <icon>applications-graphics-symbolic</icon>\n\
|
||||||
\x20 <name>Pixstrip: {xml_name}</name>\n\
|
\x20 <name>Pixstrip: {xml_name}</name>\n\
|
||||||
\x20 <command>{bin} --preset \"{safe_label}\" --files %F</command>\n\
|
\x20 <command>{cli_bin} process --preset \"{safe_label}\" %F</command>\n\
|
||||||
\x20 <description>Process with {xml_name} preset</description>\n\
|
\x20 <description>Process with {xml_name} preset</description>\n\
|
||||||
\x20 <patterns>*.jpg;*.jpeg;*.png;*.webp;*.gif;*.tiff;*.tif;*.avif;*.bmp</patterns>\n\
|
\x20 <patterns>*.jpg;*.jpeg;*.png;*.webp;*.gif;*.tiff;*.tif;*.avif;*.bmp</patterns>\n\
|
||||||
\x20 <image-files/>\n\
|
\x20 <image-files/>\n\
|
||||||
@@ -356,7 +376,7 @@ fn install_thunar() -> Result<()> {
|
|||||||
</action>\n",
|
</action>\n",
|
||||||
xml_name = name.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """),
|
xml_name = name.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """),
|
||||||
safe_label = shell_safe(name),
|
safe_label = shell_safe(name),
|
||||||
bin = bin,
|
cli_bin = cli_bin,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +412,8 @@ fn install_dolphin() -> Result<()> {
|
|||||||
let dir = dolphin_service_dir();
|
let dir = dolphin_service_dir();
|
||||||
std::fs::create_dir_all(&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 presets = get_preset_names();
|
||||||
|
|
||||||
let mut desktop = format!(
|
let mut desktop = format!(
|
||||||
@@ -406,8 +427,8 @@ fn install_dolphin() -> Result<()> {
|
|||||||
[Desktop Action Open]\n\
|
[Desktop Action Open]\n\
|
||||||
Name=Open in Pixstrip...\n\
|
Name=Open in Pixstrip...\n\
|
||||||
Icon=applications-graphics-symbolic\n\
|
Icon=applications-graphics-symbolic\n\
|
||||||
Exec={bin} --files %F\n\n",
|
Exec={gtk_bin} %F\n\n",
|
||||||
bin = bin,
|
gtk_bin = gtk_bin,
|
||||||
preset_actions = presets
|
preset_actions = presets
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
@@ -421,11 +442,11 @@ fn install_dolphin() -> Result<()> {
|
|||||||
"[Desktop Action Preset{i}]\n\
|
"[Desktop Action Preset{i}]\n\
|
||||||
Name={name}\n\
|
Name={name}\n\
|
||||||
Icon=applications-graphics-symbolic\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,
|
i = i,
|
||||||
name = name,
|
name = name,
|
||||||
safe_label = shell_safe(name),
|
safe_label = shell_safe(name),
|
||||||
bin = bin,
|
cli_bin = cli_bin,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
let subfolder_choice: Rc<RefCell<Option<bool>>> = Rc::new(RefCell::new(None));
|
||||||
|
|
||||||
// Set up drag-and-drop on the entire page
|
// Set up drag-and-drop on the entire page
|
||||||
let drop_target = gtk::DropTarget::new(gtk::gio::File::static_type(), gtk::gdk::DragAction::COPY);
|
// Accept both FileList (from file managers) and single File
|
||||||
drop_target.set_types(&[gtk::gio::File::static_type()]);
|
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();
|
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 stack_ref = stack.clone();
|
||||||
let subfolder_choice = subfolder_choice.clone();
|
let subfolder_choice = subfolder_choice.clone();
|
||||||
drop_target.connect_drop(move |target, value, _x, _y| {
|
drop_target.connect_drop(move |target, value, _x, _y| {
|
||||||
if let Ok(file) = value.get::<gtk::gio::File>()
|
// Collect paths from FileList, single File, or URI text
|
||||||
&& let Some(path) = file.path()
|
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() {
|
if path.is_dir() {
|
||||||
let has_subdirs = has_subfolders(&path);
|
let has_subdirs = has_subfolders(&path);
|
||||||
if !has_subdirs {
|
if !has_subdirs {
|
||||||
let mut files = loaded_files.borrow_mut();
|
let mut files = loaded_files.borrow_mut();
|
||||||
add_images_flat(&path, &mut files);
|
add_images_flat(&path, &mut files);
|
||||||
let count = files.len();
|
|
||||||
drop(files);
|
drop(files);
|
||||||
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
|
|
||||||
} else {
|
} else {
|
||||||
let choice = *subfolder_choice.borrow();
|
let choice = *subfolder_choice.borrow();
|
||||||
match choice {
|
match choice {
|
||||||
Some(true) => {
|
Some(true) => {
|
||||||
let mut files = loaded_files.borrow_mut();
|
let mut files = loaded_files.borrow_mut();
|
||||||
add_images_from_dir(&path, &mut files);
|
add_images_from_dir(&path, &mut files);
|
||||||
let count = files.len();
|
|
||||||
drop(files);
|
drop(files);
|
||||||
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
|
|
||||||
}
|
}
|
||||||
Some(false) => {
|
Some(false) => {
|
||||||
let mut files = loaded_files.borrow_mut();
|
let mut files = loaded_files.borrow_mut();
|
||||||
add_images_flat(&path, &mut files);
|
add_images_flat(&path, &mut files);
|
||||||
let count = files.len();
|
|
||||||
drop(files);
|
drop(files);
|
||||||
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
|
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let mut files = loaded_files.borrow_mut();
|
let mut files = loaded_files.borrow_mut();
|
||||||
add_images_flat(&path, &mut files);
|
add_images_flat(&path, &mut files);
|
||||||
let count = files.len();
|
|
||||||
drop(files);
|
drop(files);
|
||||||
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
|
|
||||||
|
|
||||||
let loaded_files = loaded_files.clone();
|
let loaded_files = loaded_files.clone();
|
||||||
let excluded = excluded.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) {
|
} else if is_image_file(&path) {
|
||||||
let mut files = loaded_files.borrow_mut();
|
let mut files = loaded_files.borrow_mut();
|
||||||
if !files.contains(&path) {
|
if !files.contains(&path) {
|
||||||
files.push(path);
|
files.push(path);
|
||||||
}
|
}
|
||||||
let count = files.len();
|
|
||||||
drop(files);
|
drop(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = loaded_files.borrow().len();
|
||||||
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
|
refresh_grid(&stack_ref, &loaded_files, &excluded, &sizes, count);
|
||||||
return true;
|
true
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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()
|
adw::NavigationPage::builder()
|
||||||
.title("Add Images")
|
.title("Add Images")
|
||||||
.tag("step-images")
|
.tag("step-images")
|
||||||
|
|||||||
Reference in New Issue
Block a user