/// Open a playlist file and stage it for review. Parsing and resolution only — /// nothing is written until `playlist_import_commit`. /// /// The phone has no dialog here, so it picks the file itself (the storage /// plugin's document picker) and hands the path in — with the picked /// document's own `name` beside it, because a document that had no path of /// its own arrives as a copy in the app's cache, and the NAME is what /// `detect_format` reads the extension off and what titles the draft. The /// desktop passes neither and opens its own dialog. #[tauri::command] pub async fn playlist_import_probe( app: tauri::AppHandle, lib: State<'_, LibraryState>, path: Option, name: Option, ) -> Result, String> { let picked = match path { Some(p) if !p.trim().is_empty() => Some(p), _ => { let (tx, rx) = tokio::sync::oneshot::channel::>(); #[cfg(desktop)] { use tauri_plugin_dialog::DialogExt; app.dialog() .file() .add_filter( "Playlist", &["m3u", "m3u8", "pls", "xspf", "csv", "json", "txt"], ) .pick_file(move |path| { let _ = tx.send(path.map(|p| p.to_string())); }); } #[cfg(mobile)] { let _ = &app; let _ = tx.send(None); } rx.await.unwrap_or(None) } }; let Some(source) = picked else { return Ok(None); }; let source_path = PathBuf::from(&source); // The picked name wins over the path's own: a cache copy is named after // the document, but only the document's name is guaranteed to be it. let name = match name { Some(n) if !n.trim().is_empty() => n, _ => source_path .file_name() .map(|n| n.to_string_lossy().into_owned()) .unwrap_or_else(|| source.clone()), };