From 21c5a4f34a33795d7836ff673a360ef1472f7567 Mon Sep 17 00:00:00 2001 From: Frieder Hannenheim Date: Mon, 16 Feb 2026 15:41:35 +0000 Subject: [PATCH] feat(dnd_destination): xdg file transfer portal support Requires the `xdg-portal` feature to be enabled to use these features. - Adds `DndDestination::on_file_transfer` method to handle `application/vnd.portal.filetransfer` drop requests - Adds `command::file_transfer_receive` function to handle the file transfer request messages - Adds `command::file_transfer_send` to initiate a file transfer from the application --- src/command.rs | 24 ++++++++++++++++++++++++ src/widget/dnd_destination.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/command.rs b/src/command.rs index 73c900c1..14d326b4 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,6 +1,9 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +#[cfg(feature = "xdg-portal")] +use std::os::fd::AsFd; + use iced::window; /// Initiates a window drag. @@ -43,3 +46,24 @@ pub fn set_windowed(id: window::Id) -> iced::Task> { pub fn toggle_maximize(id: window::Id) -> iced::Task> { iced_runtime::window::toggle_maximize(id) } + +#[cfg(feature = "xdg-portal")] +pub fn file_transfer_send(writeable: bool, auto_stop: bool, files: Vec) -> iced::Task> { + iced::Task::future(async move { + let file_transfer = ashpd::documents::FileTransfer::new().await?; + let key = file_transfer.start_transfer(writeable, auto_stop).await?; + file_transfer.add_files(&key, &files).await?; + Ok(key) + }) +} + +/// Receive the files offered over the xdg share portal using the `key`. +/// Returns a list of file paths. +#[cfg(feature = "xdg-portal")] +pub fn file_transfer_receive(key: String) -> iced::Task>> { + dbg!(&key); + iced::Task::future(async move { + let file_transfer = ashpd::documents::FileTransfer::new().await?; + file_transfer.retrieve_files(&key).await + }) +} diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 947d2fe3..a32a9fba 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -40,6 +40,8 @@ pub fn dnd_destination_for_data<'a, T: AllowedMimeTypes, Message: 'static>( static DRAG_ID_COUNTER: AtomicU64 = AtomicU64::new(0); const DND_DEST_LOG_TARGET: &str = "libcosmic::widget::dnd_destination"; +#[cfg(feature = "xdg-portal")] +pub const FILE_TRANSFER_MIME: &str = "application/vnd.portal.filetransfer"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct DragId(pub u128); @@ -73,6 +75,8 @@ pub struct DndDestination<'a, Message> { on_action_selected: Option Message>>, on_data_received: Option) -> Message>>, on_finish: Option, DndAction, f64, f64) -> Message>>, + #[cfg(feature = "xdg-portal")] + on_file_transfer: Option Message>>, } impl<'a, Message: 'static> DndDestination<'a, Message> { @@ -99,6 +103,8 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { on_action_selected: None, on_data_received: None, on_finish: None, + #[cfg(feature = "xdg-portal")] + on_file_transfer: None, } } @@ -124,6 +130,8 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { on_finish: Some(Box::new(move |mime, data, action, _, _| { on_finish(T::try_from((data, mime)).ok(), action) })), + #[cfg(feature = "xdg-portal")] + on_file_transfer: None, } } @@ -159,6 +167,8 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { on_action_selected: None, on_data_received: None, on_finish: None, + #[cfg(feature = "xdg-portal")] + on_file_transfer: None, } } @@ -237,6 +247,20 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { self } + /// Add a message that will be emitted instead of [`on_data_received`](Self::on_data_received) if the dropped files + /// are offered through the xdg share portal. You can then use [`crate::command::file_transfer_receive`] + /// with the key to receive the files. + #[cfg(feature = "xdg-portal")] + #[must_use] + pub fn on_file_transfer(mut self, f: impl Fn(String) -> Message + 'static) -> Self { + match self.mime_types.iter().position(|v| v == "text/uri-list") { + Some(i) => self.mime_types.insert(i, Cow::Borrowed(FILE_TRANSFER_MIME)), + None => self.mime_types.push(Cow::Borrowed(FILE_TRANSFER_MIME)), + } + self.on_file_transfer = Some(Box::new(f)); + self + } + /// Returns the drag id of the destination. /// /// # Panics @@ -496,6 +520,13 @@ impl Widget "offer data id={my_id:?} mime={mime_type:?} bytes={}", data.len() ); + + #[cfg(feature = "xdg-portal")] + if mime_type == FILE_TRANSFER_MIME && let Some(f) = self.on_file_transfer.as_ref() && let Ok(s) = String::from_utf8(data[..data.len() - 1].to_vec()) { + shell.publish(f(s)); + return event::Status::Captured; + } + if let (Some(msg), ret) = state.on_data_received( mime_type, data,