Files
SoulStream/handoff/rounds/patch-round18.cjs
T
KeelrLeek 9f0601027c 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.
2026-09-14 07:57:41 -05:00

114 lines
7.0 KiB
JavaScript

// Round eighteen: the hub's raw-search filters on the phone's network
// results (free slot only; FLAC or MP3 320+), and the lyrics door the
// desktop has for pasted words (plain text, or LRC with timestamps).
const fs = require('fs');
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
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); };
// ── Search: the filters ──
load('src/mobile/screens/Search.svelte');
rep(` function playPeer(g, i) {`, ` /* the hub's filters over the raw results: a free slot only, a format */
let netFree = $state(false);
let netFmt = $state("any");
const shownGroups = $derived(net.groups.filter((g) => {
if (netFree && !g.slot) return false;
if (netFmt === "flac") return g.topExt === "flac";
if (netFmt === "mp3320") return g.topExt !== "flac" && (g.maxBitrate ?? 0) >= 320;
return true;
}));
function playPeer(g, i) {`);
rep(` {#if net.error}<div class="sec-note">{net.error}</div>{/if}`,
` {#if net.error}<div class="sec-note">{net.error}</div>{/if}
{#if net.groups.length}
<div class="chips netfilters" style="padding-bottom: 10px">
<button class="chip" class:on={netFree} class:quiet={!netFree} onclick={() => (netFree = !netFree)}>Free slot</button>
{#each [["any", "Any format"], ["flac", "FLAC"], ["mp3320", "MP3 320+"]] as [v, l] (v)}
<button class="chip" class:on={netFmt === v} class:quiet={netFmt !== v} onclick={() => (netFmt = v)}>{l}</button>
{/each}
</div>
{#if shownGroups.length !== net.groups.length}<div class="sec-note">{shownGroups.length} of {net.groups.length} folders</div>{/if}
{/if}`);
rep(` {#each net.groups.slice(0, 40) as g (g.key)}`, ` {#each shownGroups.slice(0, 40) as g (g.key)}`);
save();
// ── lyrics: pasted words ──
load('src/mobile/lyrics.svelte.js');
s = s.replace(/\s*$/, String.fromCharCode(10)) + `
/** Pasted words: LRC with timestamps becomes synced, anything else plain. */
export async function pasteLyrics(text) {
const id = identity();
const t = String(text || "").trim();
if (!id || !t) return false;
try {
const p = await api.lyricsSaveText(id.title, id.artist, id.durationSecs, t);
if (p && p.status) apply(p); else await loadLyrics(true);
return true;
} catch (e) {
player.error = String(e);
return false;
}
}
`;
save();
load('src/mobile/HalfSheet.svelte');
rep(` import { lyr, nudge, searchCandidates, closeCandidates, chooseCandidate, forgetLyrics } from "./lyrics.svelte.js";`,
` import { lyr, nudge, searchCandidates, closeCandidates, chooseCandidate, forgetLyrics, pasteLyrics } from "./lyrics.svelte.js";`);
rep(` {@render row("search", "Wrong lyrics? Search again", "LRCLIB, by this track's name", () => { void searchCandidates(); })}`,
` {@render row("search", "Wrong lyrics? Search again", "LRCLIB, by this track's name", () => { void searchCandidates(); })}
{@render row("file-text", "Paste lyrics…", "plain text, or LRC with [mm:ss] timestamps", () => openHalf("prompt", { title: "Paste lyrics", placeholder: "[00:12.50] The first line…", ok: "Save", multiline: true, onsubmit: (v) => { void pasteLyrics(v); } }))}`);
save();
// ── mocks ──
load('src/lib/mockLibrary.js');
rep(` if (cmd === "lyrics_set_offset") return Promise.resolve();`,
` if (cmd === "lyrics_set_offset") return Promise.resolve();
if (cmd === "lyrics_save_text") {
const lines = String(args?.text || "").split(/\\r?\\n/).map((l) => l.trim()).filter(Boolean);
const timed = lines.map((l) => { const m = /^\\[(\\d+):(\\d\\d(?:\\.\\d+)?)\\]\\s*(.*)$/.exec(l); return m ? { time_ms: Math.round((+m[1] * 60 + +m[2]) * 1000), text: m[3] } : null; });
const synced = timed.every(Boolean);
return Promise.resolve({ status: synced ? "synced" : "plain", source: "pasted", synced: synced ? timed : [], plain: synced ? null : lines.join("\\n"), offset_ms: 0, auto_offset_ms: 0, auto_sync_off: false });
}`);
save();
// ── the walks ──
load('tests/mobile/smoke.mjs');
rep(` { do: \`__click('.halfsheet .row', 'Live')\`, name: "lyrics-chosen", wait: 700 },`,
` { do: \`__click('.halfsheet .row', 'Live')\`, name: "lyrics-chosen", wait: 700 },
/* pasted words, timestamped: synced lines */
{ do: \`__click('.lyr-tools', null)\`, wait: 600 },
{ do: \`__click('.halfsheet .row', 'Paste lyrics')\`, wait: 600 },
{ do: \`(function () { var i = document.querySelector('.halfsheet .prompt .field'); i.value = '[00:01.00] Pasted line one\\\\n[00:05.00] Pasted line two'; i.dispatchEvent(new Event('input', { bubbles: true })); })()\`, wait: 300 },
{ do: \`__click('.halfsheet .prompt-acts .btn-fill', 'Save')\`, name: "lyrics-pasted", wait: 800 },`);
rep(` "lyrics-chosen": (i) => !i.half.includes("up") && i.lyrLines >= 3,`,
` "lyrics-chosen": (i) => !i.half.includes("up") && i.lyrLines >= 3,
"lyrics-pasted": (i) => !i.half.includes("up") && i.lyrLines === 2,`);
rep(` { do: \`document.querySelector('.peer-file').dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))\`, name: "peer-file-sheet", wait: 700 },`,
` /* the hub's filters: FLAC only, then a free slot only */
{ do: \`__click('.netfilters .chip', 'FLAC')\`, name: "net-filter-flac", wait: 500 },
{ do: \`__click('.netfilters .chip', 'Free slot')\`, name: "net-filter-free", wait: 500 },
{ do: \`__click('.netfilters .chip', 'Free slot')\`, wait: 300 },
{ do: \`__click('.netfilters .chip', 'Any format')\`, wait: 400 },
{ do: \`document.querySelector('.peer-file').dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))\`, name: "peer-file-sheet", wait: 700 },`);
rep(` "peer-file-sheet": (i) => i.half.includes("up") && /Save this file/.test(i.halfText) && /Play from here/.test(i.halfText),`,
` "net-filter-flac": (i) => i.peers === 3,
"net-filter-free": (i) => i.peers === 2,
"peer-file-sheet": (i) => i.half.includes("up") && /Save this file/.test(i.halfText) && /Play from here/.test(i.halfText),`);
save();
// ── the doc ──
load('ANDROID.md');
rep(`The nav wears two badges the desktop's chrome has:`, `The network results carry the hub's filters (a free slot only; FLAC or
MP3 320+), and the lyrics sheet has the desktop's paste door (plain text,
or LRC with timestamps, kept for the track). The nav wears two badges the desktop's chrome has:`);
save();
console.log('round eighteen wired');