Everything written this session that has not been through the device: rounds 43 to 48 (each patch adversarially verified, 43's transient-notice expectation fixed), a partial round 49, the pipeline scripts, the Android Auto design and the icon comparison mock. Also the two harness changes that were never A/B'd — adaptive waits, which may only shorten a declared wait and never lengthen it, and a ship that runs the mock walk and the deploy at the same time. The A/B and its 255-step baseline are in handoff/tools; main stays at what the phone has actually walked.
55 lines
2.1 KiB
Plaintext
55 lines
2.1 KiB
Plaintext
/// 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<String>,
|
|
name: Option<String>,
|
|
) -> Result<Option<ImportDraft>, String> {
|
|
let picked = match path {
|
|
Some(p) if !p.trim().is_empty() => Some(p),
|
|
_ => {
|
|
let (tx, rx) = tokio::sync::oneshot::channel::<Option<String>>();
|
|
#[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()),
|
|
};
|