From fdbf80b3fe40ae11347de8c0efc72993b0c89c04 Mon Sep 17 00:00:00 2001 From: nilsk Date: Tue, 21 Jul 2026 13:20:19 +0200 Subject: [PATCH] add share sheet implementation with platform-specific support for Windows, macOS, and Linux --- runtime/src/share_sheet.rs | 131 +++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 runtime/src/share_sheet.rs diff --git a/runtime/src/share_sheet.rs b/runtime/src/share_sheet.rs new file mode 100644 index 00000000..fa8afe25 --- /dev/null +++ b/runtime/src/share_sheet.rs @@ -0,0 +1,131 @@ +use axum::Json; +use log::{error, info}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use crate::api_token::APIToken; + +#[derive(Deserialize)] +pub struct ShareFileRequest { + file_path: String, +} + +#[derive(Serialize)] +pub struct ShareFileResponse { + success: bool, + issue: String, +} + +pub async fn share_file(_token: APIToken, Json(request): Json) -> Json { + let path = PathBuf::from(request.file_path.trim()); + if path.as_os_str().is_empty() { + return failure("The file path is empty."); + } + + if !path.is_file() { + return failure(format!("The requested path is not an existing file: {}", path.to_string_lossy())); + } + + let result = share_file_on_platform(path).await; + match result { + Ok(()) => { + info!(Source = "Share sheet"; "Opened the native share UI."); + Json(ShareFileResponse { + success: true, + issue: String::new(), + }) + } + + Err(issue) => { + error!(Source = "Share sheet"; "{issue}"); + failure(issue) + } + } +} + +fn failure(issue: impl Into) -> Json { + Json(ShareFileResponse { + success: false, + issue: issue.into(), + }) +} + +#[cfg(target_os = "linux")] +async fn share_file_on_platform(path: PathBuf) -> Result<(), String> { + crate::file_actions::open_existing_file_in_file_manager(path).await +} + +#[cfg(windows)] +async fn share_file_on_platform(path: PathBuf) -> Result<(), String> { + use windows::ApplicationModel::DataTransfer::{DataRequestedEventArgs, DataTransferManager}; + use windows::Foundation::TypedEventHandler; + use windows::Storage::{IStorageItem, StorageFile}; + use windows::Win32::UI::Shell::IDataTransferManagerInterop; + use windows::core::{factory, HSTRING, Interface}; + use windows_collections::IIterable; + + let window = crate::app_window::MAIN_WINDOW.lock().unwrap().clone() + .ok_or_else(|| String::from("The main window is not available."))?; + let ui_window = window.clone(); + let path = path.to_string_lossy().to_string(); + let (sender, receiver) = tokio::sync::oneshot::channel(); + + window.run_on_main_thread(move || { + let result = (|| -> Result<(), String> { + let hwnd = ui_window.hwnd().map_err(|error| format!("Failed to get the native window handle: {error}"))?; + let interop: IDataTransferManagerInterop = factory::() + .map_err(|error| format!("Failed to access the Windows share service: {error}"))?; + let manager: DataTransferManager = unsafe { interop.GetForWindow(hwnd) } + .map_err(|error| format!("Failed to create the Windows share request: {error}"))?; + let handler = TypedEventHandler::::new(move |_, arguments| { + let Some(arguments) = arguments.as_ref() else { + return Ok(()); + }; + + let request = arguments.Request()?; + let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(&path))?.get()?; + let file: IStorageItem = file.cast()?; + let items = IIterable::::from(vec![Some(file)]); + request.Data()?.Properties()?.SetTitle(&HSTRING::from("MindWork AI Studio"))?; + request.Data()?.SetStorageItemsReadOnly(&items)?; + Ok(()) + }); + manager.DataRequested(&handler) + .map_err(|error| format!("Failed to provide the shared file: {error}"))?; + unsafe { interop.ShowShareUIForWindow(hwnd) } + .map_err(|error| format!("Failed to open the Windows share sheet: {error}"))?; + Ok(()) + })(); + let _ = sender.send(result); + }).map_err(|error| format!("Failed to schedule the Windows share sheet: {error}"))?; + + receiver.await.map_err(|_| String::from("The Windows share sheet did not return a result."))? +} + +#[cfg(target_os = "macos")] +async fn share_file_on_platform(path: PathBuf) -> Result<(), String> { + use objc2::MainThreadMarker; + use objc2_app_kit::{NSRectEdge, NSSharingServicePicker, NSView}; + use objc2_foundation::{NSArray, NSRect, NSString, NSURL}; + + let window = crate::app_window::MAIN_WINDOW.lock().unwrap().clone() + .ok_or_else(|| String::from("The main window is not available."))?; + let ui_window = window.clone(); + let path = path.to_string_lossy().to_string(); + let (sender, receiver) = tokio::sync::oneshot::channel(); + + window.run_on_main_thread(move || { + let result = (|| -> Result<(), String> { + let mtm = MainThreadMarker::new().ok_or_else(|| String::from("The macOS share sheet must run on the main thread."))?; + let path = NSString::from_str(&path); + let url = NSURL::fileURLWithPath(&path); + let items = NSArray::from_retained_slice(&[url]); + let picker = NSSharingServicePicker::alloc(mtm).initWithItems(&items); + let view = unsafe { &*ui_window.ns_view().map_err(|error| format!("Failed to get the native view: {error}"))?.cast::() }; + picker.showRelativeToRect_ofView_preferredEdge(NSRect::ZERO, view, NSRectEdge::MinY); + Ok(()) + })(); + let _ = sender.send(result); + }).map_err(|error| format!("Failed to schedule the macOS share sheet: {error}"))?; + + receiver.await.map_err(|_| String::from("The macOS share sheet did not return a result."))? +}