android: a playlist, out of the phone
The desktop has had Export since playlists v2 and the phone had no door onto it for one honest reason: `tauri-plugin-dialog` does open Android's create-document sheet, but it answers with a `content://` URI and `playlist_export` wrote with `std::fs::write`, which cannot take one. The storage plugin grew a `create_document` whose Kotlin writes the bytes through the ContentResolver — a plugin function called from Rust, not a command, so the file never passes through the WebView and nothing was added to the ACL — and `playlist_export` now builds the file above the platform fork with the same `playlist_formats` writers, so the phone rebuilds no format and the two exports cannot drift. The door is the playlist's own sheet, on the page and on the shelf card, with the desktop's five profiles in the desktop's order; a dismissed sheet says nothing, because nothing happened. The mock walk drives it three ways — a file written where the sheet pointed, a sheet dismissed, a refusal read back as a sentence — and the phone walk opens the real sheet and dismisses it with BACK, writing nothing to the phone. The Kotlin is unbuilt: nothing in this tree compiles it, though it is written against tauri-android's own `PluginHandle`, `Invoke` and `JSObject` rather than against a guess at them.
This commit is contained in:
1 parent
95c2bfeeba
commit
04c87e0db1
9 files changed
+390
-72
No files matched your search
@@ -136,6 +136,22 @@ function mockLyrics(title) {
|
||||
}
|
||||
function mockPlaylists(cmd, args) {
|
||||
if (cmd === "list_playlists") return Promise.resolve(PL.map((p) => ({ ...p })));
|
||||
/* Export. The whole file is built in Rust and written by whichever door
|
||||
the platform has — the desktop's save dialog, the phone's
|
||||
create-document sheet — so all the command ever answers is where it
|
||||
went: a path there, the document's name here. The sanitiser is the
|
||||
Rust one, character for character (and written as a code-point test
|
||||
rather than a range, so nothing in this file is a control byte). A
|
||||
dismissed sheet (null) and a refusal are armed from the walk with
|
||||
`__ssMock.answer` / `.fail`, which `weather()` answers first. */
|
||||
if (cmd === "playlist_export") {
|
||||
const p = plOf(args);
|
||||
if (!p) return Promise.reject(new Error("Playlist not found"));
|
||||
const fmt = String(args?.format ?? "");
|
||||
if (!["m3u8", "xspf", "pls", "csv", "json"].includes(fmt)) return Promise.reject(new Error("Unknown export format " + fmt));
|
||||
const safe = [...String(p.name)].map((c) => (/[<>:"\/\\|?*]/.test(c) || c.codePointAt(0) < 0x20 ? "_" : c)).join("");
|
||||
return new Promise((r) => setTimeout(() => r(safe + "." + fmt), 120));
|
||||
}
|
||||
if (cmd === "playlist_items") return new Promise((r) => setTimeout(() => r((PL_ITEMS[Number(args?.id)] || []).map((x) => ({ ...x }))), 80));
|
||||
if (cmd === "playlist_cover") { const p = plOf(args); return Promise.resolve(p && p.cover_path ? svgArt(p.id === 1 ? 330 : 280, p.id === 1 ? "L" : "M") : null); }
|
||||
if (cmd === "playlist_cover_set") { const p = plOf(args); if (!p) return Promise.reject(new Error("no such playlist")); p.cover_path = String(args?.path || "mock://photo"); plTouch(p); return Promise.resolve(svgArt(300, "P")); }
|
||||
|
||||
Reference in new issue
Block a user