android: the handoff — the pending rounds, the pipeline, and what is unverified
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.
This commit is contained in:
1 parent
867fd42972
commit
9f0601027c
400 files changed
+26401
-3
No files matched your search
@@ -0,0 +1,188 @@
|
||||
// Round thirty-five: a playlist, out of the phone. The desktop has had
|
||||
// Export since playlists v2 and the phone has never had a 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` writes with `std::fs::write`, which cannot take one.
|
||||
// So: a `create_document` in the storage plugin whose Kotlin writes the
|
||||
// bytes through the ContentResolver, the export restructured so the file
|
||||
// is built by `playlist_formats` ABOVE the platform fork (the phone must
|
||||
// never rebuild a format — two exports that drift are worse than one that
|
||||
// is missing), and the desktop's five profiles on the playlist's own
|
||||
// sheet, in the desktop's order and its own words. `create_document` is a
|
||||
// plugin function called from RUST, not a command: the file never passes
|
||||
// through the WebView, so build.rs, the permissions and the capability are
|
||||
// all untouched.
|
||||
const fs = require('fs');
|
||||
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
|
||||
const scratch = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/';
|
||||
const ins = (name) => fs.readFileSync(scratch + name, 'utf8').replace(/\s+$/, '');
|
||||
let p, s;
|
||||
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
|
||||
const save = () => fs.writeFileSync(root + p, s);
|
||||
const once = (a) => {
|
||||
const i = s.indexOf(a);
|
||||
if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90));
|
||||
if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90));
|
||||
return i;
|
||||
};
|
||||
const rep = (a, b) => { once(a); s = s.replace(a, () => b); };
|
||||
/* One whole function goes, doc comment and all — too long to spell out
|
||||
twice, so it is cut between two anchors `once` has vouched for. */
|
||||
const cut = (from, to, b) => {
|
||||
const i = once(from);
|
||||
const j = once(to);
|
||||
if (j < i) throw new Error('end before start in ' + p + ': ' + from.slice(0, 60));
|
||||
s = s.slice(0, i) + b + s.slice(j + to.length);
|
||||
};
|
||||
const NL = String.fromCharCode(10);
|
||||
|
||||
/* Several of the anchors below are re-emitted (the sleep branch, the two
|
||||
walk-array ends), so a second run would double-insert where `once`
|
||||
cannot see it coming and only throw further down, leaving half a tree.
|
||||
This round creates no new file, so the guard is what it adds to the one
|
||||
crate nothing else in the repo touches. */
|
||||
if (fs.readFileSync(root + 'crates/tauri-plugin-storage/src/lib.rs', 'utf8').includes('create_document')) {
|
||||
throw new Error('tauri-plugin-storage already has create_document — round 35 has already run against ' + root);
|
||||
}
|
||||
|
||||
// ── the plugin: a document written where the user points ──────────────
|
||||
/* Not a command, so `invoke_handler`, build.rs's COMMANDS, permissions/
|
||||
and src-tauri/capabilities are all DELIBERATELY untouched: the ACL gates
|
||||
`invoke`, and nothing here is invoked from the WebView. */
|
||||
load('crates/tauri-plugin-storage/src/lib.rs');
|
||||
rep(`pub fn init<R: Runtime>() -> TauriPlugin<R> {`,
|
||||
ins('ins-r35-storage-rs.txt') + NL + NL + `pub fn init<R: Runtime>() -> TauriPlugin<R> {`);
|
||||
save();
|
||||
|
||||
// ── the Kotlin half. UNBUILT: nothing in this session can compile it —
|
||||
// it is written against pickImage/imagePicked next door and against
|
||||
// TransferNoticePlugin's @InvokeArg/parseArgs pair, and no further.
|
||||
load('crates/tauri-plugin-storage/android/src/main/java/com/soulstream/storage/StoragePlugin.kt');
|
||||
rep(`import android.provider.Settings`,
|
||||
`import android.provider.OpenableColumns
|
||||
import android.provider.Settings`);
|
||||
rep(`import app.tauri.annotation.TauriPlugin`,
|
||||
`import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.annotation.TauriPlugin`);
|
||||
/* before the class KDoc, never between a KDoc and what it documents */
|
||||
rep(`/**
|
||||
* The phone's storage for the library:`,
|
||||
ins('ins-r35-kt-args.txt') + NL + NL + `/**
|
||||
* The phone's storage for the library:`);
|
||||
/* a blank line ahead of it: the file has none before pickImage's KDoc, and
|
||||
two members running together is the one thing it does not do elsewhere */
|
||||
rep(` /** A photo from the phone: the document picker limited to images, the`,
|
||||
NL + ins('ins-r35-kt-command.txt') + NL + NL + ` /** A photo from the phone: the document picker limited to images, the`);
|
||||
save();
|
||||
|
||||
// ── the export itself: one command, restructured around the fork ──────
|
||||
load('src-tauri/src/playlists.rs');
|
||||
cut(`/// Export a playlist through a save dialog. \`format\` ∈ m3u8 | xspf | csv |`,
|
||||
` Ok(Some(dest))${NL}}`,
|
||||
ins('ins-r35-export-rs.txt'));
|
||||
save();
|
||||
|
||||
// ── the wrapper's comment. No new wrapper: JS never calls the plugin ──
|
||||
load('src/lib/api.js');
|
||||
rep(`// Export via a save dialog; format ∈ m3u8 | xspf | csv | json. Resolves to
|
||||
// the written path, or null when the dialog was cancelled.`,
|
||||
`// Export through whichever save door the platform has; format ∈ m3u8 |
|
||||
// xspf | pls | csv | json. Resolves to where the file went — the written
|
||||
// path on the desktop, the document's name on the phone (Android's
|
||||
// create-document sheet answers a content:// URI, not a path) — or null
|
||||
// when that sheet was dismissed.`);
|
||||
save();
|
||||
|
||||
// ── the store: the desktop's five, and the one call ───────────────────
|
||||
load('src/mobile/playlists.svelte.js');
|
||||
rep(`import { note } from "./notice.svelte.js";`,
|
||||
`import { note } from "./notice.svelte.js";
|
||||
import { basename } from "./util.js";`);
|
||||
rep(`/* ── reorder: a row up, down or to the top; the complete id order goes`,
|
||||
ins('ins-r35-store.txt') + NL + NL + `/* ── reorder: a row up, down or to the top; the complete id order goes`);
|
||||
save();
|
||||
|
||||
// ── the door: the playlist's own sheet, and the sheet it opens ────────
|
||||
load('src/mobile/HalfSheet.svelte');
|
||||
rep(`coverFromPhoto, coverClear } from "./playlists.svelte.js";`,
|
||||
`coverFromPhoto, coverClear, EXPORTS, exportPlaylist } from "./playlists.svelte.js";`);
|
||||
/* unguarded by `kind`, so the shelf card's sheet carries it too — the
|
||||
desktop has Export on BOTH menus (Playlists.svelte and Playlist.svelte) */
|
||||
rep(` <div class="sheet-hair"></div>
|
||||
{@render row("trash", "Delete playlist", "", () => confirmDelete(p))}`,
|
||||
` <div class="sheet-hair"></div>
|
||||
{@render row("export", "Export…", p.track_count + " track" + (p.track_count === 1 ? "" : "s") + ", as a playlist file", () => openHalf("plexport", { playlist: p }))}
|
||||
<div class="sheet-hair"></div>
|
||||
{@render row("trash", "Delete playlist", "", () => confirmDelete(p))}`);
|
||||
rep(` {:else if kind === "sleep"}`,
|
||||
ins('ins-r35-sheet.txt') + NL + ` {:else if kind === "sleep"}`);
|
||||
save();
|
||||
|
||||
// ── the mock: where the file went, and nothing else ───────────────────
|
||||
load('src/lib/mockLibrary.js');
|
||||
rep(` if (cmd === "list_playlists") return Promise.resolve(PL.map((p) => ({ ...p })));`,
|
||||
` if (cmd === "list_playlists") return Promise.resolve(PL.map((p) => ({ ...p })));
|
||||
` + ins('ins-r35-mock.txt'));
|
||||
save();
|
||||
|
||||
// ── the walks. The mock's two anchors are the ARRAY ENDS, so this block
|
||||
// lands after round thirty-four's fresh install put a provisioned
|
||||
// device back — which is exactly the device it wants.
|
||||
load('tests/mobile/smoke.mjs');
|
||||
rep(`];
|
||||
const MOCK_EXPECT = {`, ins('ins-r35-mock-steps.txt') + NL + `];
|
||||
const MOCK_EXPECT = {`);
|
||||
rep(`};
|
||||
|
||||
/* the phone walk: a record by name, played for real */`,
|
||||
ins('ins-r35-mock-expect.txt') + NL + `};
|
||||
|
||||
/* the phone walk: a record by name, played for real */`);
|
||||
/* the phone leg rides the "Walk test" playlist the walk itself makes and
|
||||
deletes two steps later; nothing is written to Kyle's storage */
|
||||
rep(` { do: \`document.querySelectorAll('.gbar .iconbtn')[1].click()\`, name: "playlist-more", wait: 800 },`,
|
||||
ins('ins-r35-phone-steps.txt') + NL + ` { do: \`document.querySelectorAll('.gbar .iconbtn')[1].click()\`, name: "playlist-more", wait: 800 },`);
|
||||
rep(` end: (i) => /^(Good |Still)/.test(i.title),`,
|
||||
ins('ins-r35-phone-expect.txt') + NL + ` end: (i) => /^(Good |Still)/.test(i.title),`);
|
||||
save();
|
||||
|
||||
// ── the doc ───────────────────────────────────────────────────────────
|
||||
load('ANDROID.md');
|
||||
rep(`- \`pick_folder\` answers "nothing picked" (desktop-only dialog); \`save_file\` works;`,
|
||||
`- \`pick_folder\` answers "nothing picked" (desktop-only dialog); \`save_file\`
|
||||
opens Android's create-document sheet, but answers a \`content://\` URI that
|
||||
\`std::fs\` cannot open — which is why the playlist export goes through the
|
||||
storage plugin's \`create_document\` instead;`);
|
||||
rep(`- **The share sheet** (\`crates/tauri-plugin-share\`, \`share.svelte.js\`):`,
|
||||
ins('ins-r35-doc-export.txt') + NL + `- **The share sheet** (\`crates/tauri-plugin-share\`, \`share.svelte.js\`):`);
|
||||
rep(`Not on the phone yet: the playlist-file door and export — both want a file
|
||||
picker the storage plugin has still to grow — the listener door (it needs
|
||||
yt-dlp, a desktop tool), drag reorder, and Share on SoulStream.`,
|
||||
`Not on the phone yet: the playlist-file door — it wants a file picker the
|
||||
storage plugin has still to grow — the listener door (it needs yt-dlp, a
|
||||
desktop tool), drag reorder, and Share on SoulStream.`);
|
||||
rep(`it answered), then back to the provisioned device, asserts what each step should show`,
|
||||
`it answered), then back to the provisioned device, and after that the
|
||||
playlist export door three ways (a file written where the sheet pointed, a
|
||||
dismissed sheet that says nothing at all, and a refusal read back as a
|
||||
sentence), asserts what each step should show`);
|
||||
/* nine named weather steps become twelve: the dismissed sheet (`answer`),
|
||||
the refusal (`fail`) and the one that reads the weather back off
|
||||
(`clear`) — the same three roles `weather-clear` was counted in */
|
||||
rep(`Nine mock steps drive them`, `Twelve mock steps drive them`);
|
||||
/* the last line of that bullet — "beside the `pick_folder` and `pick_image`
|
||||
it already has" — is left BYTE-IDENTICAL so a sibling round adding
|
||||
`pick_file` still finds its anchor. It is now incomplete (create_document
|
||||
is there too); incomplete beats breaking the chain. */
|
||||
rep(`- **Waiting on Rust, not on a picker.** Playlist EXPORT: \`tauri-plugin-dialog\`
|
||||
does open Android's create-document sheet from Rust, but it answers with a
|
||||
\`content://\` URI and \`playlist_export\` writes with \`std::fs::write\`, which
|
||||
cannot take one. It needs a ContentResolver write, or the bytes handed to
|
||||
an outbound share intent — and \`crates/tauri-plugin-share\` is inbound-only
|
||||
today. Playlist-FILE import wants a \`pick_file\` in the storage plugin,`,
|
||||
`- **Waiting on a picker.** The storage plugin can WRITE a document now
|
||||
(\`create_document\`, which the playlist export goes through) but it cannot
|
||||
read one. Playlist-FILE import wants a \`pick_file\` in the storage plugin,`);
|
||||
save();
|
||||
|
||||
console.log('round 35 wired');
|
||||
Reference in new issue
Block a user