/// What Android's create-document sheet answered: where the file went, and /// the name it actually got (the sheet lets the user change it on the way /// past). Both absent when the sheet was dismissed — a cancel is not an /// error here, because the export it serves answers a cancel with None. #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct CreatedDocument { pub uri: Option, pub name: Option, } /// What `createDocument` is handed, as the Kotlin `@InvokeArg` names it. /// Borrowed: the file can be long and nothing here keeps a copy. #[derive(Serialize)] #[serde(rename_all = "camelCase")] #[allow(dead_code)] // only the phone ever sends one; the desktop has a dialog struct CreateDocumentArgs<'a> { name: &'a str, mime: &'a str, text: &'a str, } /// A text file written wherever the user points: Android's create-document /// sheet (ACTION_CREATE_DOCUMENT), and the Kotlin side writing the bytes /// through the ContentResolver. That last part is the whole reason this /// exists — the sheet answers with a `content://` URI, and `std::fs` has /// never been able to open one, which is why the playlist export had no /// door on the phone at all. /// /// Deliberately NOT a `#[tauri::command]`: the caller is Rust, so the file /// is built where the desktop builds it and the bytes never pass through /// the WebView. Nothing is added to the ACL, and there is no JS wrapper to /// keep in step with. /// /// A dismissed sheet is not an error — it answers with both fields empty. pub fn create_document( app: &tauri::AppHandle, name: &str, mime: &str, text: &str, ) -> Result { #[cfg(mobile)] { use tauri::Manager; let storage = app.state::>(); storage .0 .run_mobile_plugin::( "createDocument", CreateDocumentArgs { name, mime, text }, ) .map_err(|e| e.to_string()) } #[cfg(not(mobile))] { let _ = (app, name, mime, text); Err("the create-document sheet is the phone's; here a save dialog picks the path".into()) } }