Files
SoulStream/handoff/rounds/patch-round5.cjs
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

170 lines
11 KiB
JavaScript

// Round five: the catalog record page's own sheet (play, save, add all to a
// playlist, the artist), a raw file's sheet (play from here, save this
// file, browse), room users → the peer sheet, and Search's "did you mean"
// from the library's own names. Mocks, walks, doc.
const fs = require('fs');
const 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');
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); };
const NL = String.fromCharCode(10);
const afterLine = (anchor, text) => { const i = once(anchor); const nl = s.indexOf(NL, i); s = s.slice(0, nl + 1) + text + s.slice(nl + 1); };
const beforeLine = (anchor, text) => { const i = once(anchor); const ls = s.lastIndexOf(NL, i) + 1; s = s.slice(0, ls) + text + s.slice(ls); };
// ── HalfSheet ──
load('src/mobile/HalfSheet.svelte');
rep(` import { streamSong, saveSong } from "./resolve.svelte.js";`,
` import { streamSong, saveSong, streamCatalogAlbum, saveCatalogAlbum } from "./resolve.svelte.js";
import { catalog, yearOf } from "./catalog.svelte.js";`);
rep(` import { fmtOf, albumFmt, fmtBytes, basename, dirname, longDur, mmss } from "./util.js";`,
` import { fmtOf, albumFmt, fmtBytes, basename, dirname, longDur, mmss, titleFromName } from "./util.js";`);
rep(` if (t.n === "playlist") return "playlist-more";
return "home-more";`, ` if (t.n === "playlist") return "playlist-more";
if (t.n === "catalogAlbum") return "catalogAlbum-more";
return "home-more";`);
rep(` const trackArg = $derived(kind === "track" ? shown?.arg : null);`,
` const trackArg = $derived(kind === "track" ? shown?.arg : null);
/* the catalog record page's sheet reads the page the store holds */
const catPage = $derived(kind === "catalogAlbum-more" ? (catalog.albums[top().c.id]?.page ?? null) : null);`);
rep(`oncontextmenu={(e) => { e.preventDefault(); (buddy ? removeBuddy : addBuddy)(u.username).catch(() => {}); }}>`,
`oncontextmenu={(e) => { e.preventDefault(); openHalf("peer", { username: u.username }); }}>`);
beforeLine(` {:else if kind === "librarysort"}`, ins('ins-halfsheet-cat-file.txt'));
save();
// ── Search: a raw file's sheet; did you mean ──
load('src/mobile/screens/Search.svelte');
rep(` <button class="peer-file" class:playing={player.track?.path === f.path} onclick={() => playPeer(g, i)}>`,
` <button class="peer-file" class:playing={player.track?.path === f.path} onclick={() => playPeer(g, i)} oncontextmenu={(e) => { e.preventDefault(); openHalf("peerfile", { group: g, index: i }); }}>`);
rep(` const catEmpty = $derived(!!cat && !allArtists.length && !allAlbums.length && !allTracks.length);`,
` const catEmpty = $derived(!!cat && !allArtists.length && !allAlbums.length && !allTracks.length);
/* a spelling correction, when one is obvious — the desktop's rule: the
only trustworthy vocabulary offline is the names this phone knows
(the library, the searches that worked), a word is corrected only
when a real name is within two edits, nothing is guessed otherwise */
const vocabulary = $derived.by(() => {
const words = new Set();
const add = (text) => { for (const w of String(text ?? "").toLowerCase().split(/[^a-z0-9']+/)) if (w.length >= 3) words.add(w); };
for (const a of lib.albums) { add(a.artist); add(a.title); }
for (const r of recents) add(r);
return [...words];
});
const didYouMean = $derived.by(() => {
if (!catEmpty || catBusy) return null;
const typed = query.split(/\\s+/);
if (!typed.length || !vocabulary.length) return null;
let corrected = false;
const fixed = typed.map((word) => {
const lower = word.toLowerCase();
if (lower.length < 3 || vocabulary.includes(lower)) return word;
let best = null;
let bestDist = 3;
for (const c of vocabulary) { const d = editDistance(lower, c, bestDist); if (d < bestDist) { bestDist = d; best = c; } }
if (!best) return word;
corrected = true;
return best;
});
return corrected ? fixed.join(" ") : null;
});
/** Levenshtein, abandoned as soon as every cell exceeds the limit. */
function editDistance(a, b, limit) {
if (Math.abs(a.length - b.length) >= limit) return limit;
const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
for (let i = 1; i <= a.length; i++) {
let diag = prev[0];
prev[0] = i;
let row = i;
for (let j = 1; j <= b.length; j++) {
const tmp = prev[j];
prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1, diag + (a[i - 1] === b[j - 1] ? 0 : 1));
diag = tmp;
row = Math.min(row, prev[j]);
}
if (row >= limit) return limit;
}
return prev[b.length];
}`);
rep(` {#if catEmpty && !catBusy}<div class="sec-note">Nothing on the catalog for “{query}”.</div>{/if}`,
` {#if catEmpty && !catBusy}<div class="sec-note">Nothing on the catalog for “{query}”.{#if didYouMean} Did you mean <button class="dym" onclick={() => setQ(didYouMean)}>{didYouMean}</button>?{/if}</div>{/if}`);
save();
// ── Browse: a file's sheet ──
load('src/mobile/screens/Browse.svelte');
rep(` <button class="peer-file" class:playing={player.track?.path === f.path} disabled={!audio(f)} onclick={() => play(d, f)}>`,
` <button class="peer-file" class:playing={player.track?.path === f.path} disabled={!audio(f)} onclick={() => play(d, f)} oncontextmenu={(e) => { e.preventDefault(); fileSheet(d, f); }}>`);
rep(` function toggle(name) {`, ` /* a file's sheet: the folder as the sheet's group shape */
function fileSheet(d, f) {
const files = d.files.filter(audio);
openHalf("peerfile", { group: { username, folder: d.name, folderName: dirName(d), files, slot: true, speed: 0, queue: 0 }, index: Math.max(0, files.indexOf(f)) });
}
function toggle(name) {`);
save();
// ── the walks ──
load('tests/mobile/smoke.mjs');
rep(` recents: document.querySelectorAll('.recent-chips .chip').length,`,
` recents: document.querySelectorAll('.recent-chips .chip').length,
dym: (document.querySelector('.dym') || {}).textContent || '',
sq: (document.querySelector('.sfield input') || {}).value || '',`);
// did you mean: a library name within two edits
rep(` { do: \`(function () { var i = document.querySelector('.sfield input'); i.value = 'shpo'; i.dispatchEvent(new Event('input', { bubbles: true })); })()\`, name: "search", wait: 900 },`,
` { do: \`(function () { var i = document.querySelector('.sfield input'); i.value = 'shpo'; i.dispatchEvent(new Event('input', { bubbles: true })); })()\`, name: "search", wait: 900 },
/* a misspelt library name: the catalog has nothing, the library's own names correct it */
{ do: \`(function () { var i = document.querySelector('.sfield input'); i.value = 'shpongel'; i.dispatchEvent(new Event('input', { bubbles: true })); })()\`, name: "search-dym", wait: 900 },
{ do: \`__click('.dym', null)\`, name: "search-dym-pick", wait: 700 },`);
afterLine('"search-empty": (i) =>', ` "search-dym": (i) => i.dym === "shpongle",
"search-dym-pick": (i) => i.sq === "shpongle" && !i.dym,
`);
// the catalog record page's sheet, a raw file's sheet
rep(` { do: \`__click('.rail .release', 'Discovery')\`, name: "cat-album", wait: 1500 },`,
` { do: \`__click('.rail .release', 'Discovery')\`, name: "cat-album", wait: 1500 },
/* the record page's own sheet from the bar */
{ do: \`document.querySelectorAll('.gbar .iconbtn')[1].click()\`, name: "cat-album-sheet", wait: 700 },
{ do: \`document.querySelector('.scrim').click()\`, wait: 500 },`);
rep(` "cat-album": (i) => i.screen.includes("tr-push") && i.title.startsWith("Discovery") && i.eds >= 2 && i.lede && i.bars >= 10,`,
` "cat-album": (i) => i.screen.includes("tr-push") && i.title.startsWith("Discovery") && i.eds >= 2 && i.lede && i.bars >= 10,
"cat-album-sheet": (i) => i.half.includes("up") && /Save to library/.test(i.halfText) && /Add to playlist/.test(i.halfText) && /Go to artist/.test(i.halfText),`);
rep(` { do: \`document.querySelector('.peer-head .peer-menu').click()\`, name: "peer-sheet", wait: 700 },`,
` { do: \`document.querySelector('.peer-file').dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))\`, name: "peer-file-sheet", wait: 700 },
{ do: \`document.querySelector('.scrim').click()\`, wait: 500 },
{ do: \`document.querySelector('.peer-head .peer-menu').click()\`, name: "peer-sheet", wait: 700 },`);
rep(` "peer-sheet": (i) => i.half.includes("up") && /Browse their files/.test(i.halfText) && /Play this folder/.test(i.halfText),`,
` "peer-file-sheet": (i) => i.half.includes("up") && /Save this file/.test(i.halfText) && /Play from here/.test(i.halfText),
"peer-sheet": (i) => i.half.includes("up") && /Browse their files/.test(i.halfText) && /Play this folder/.test(i.halfText),`);
save();
// ── CSS ──
load('src/mobile/shell.css');
s = s.replace(/\s*$/, NL) + `
/* Search: the spelling suggestion, inline in the note */
.sec-note .dym { color: var(--accent-hover); font-weight: 750; padding: 0 2px; }
`;
save();
// ── the doc ──
load('ANDROID.md');
rep(`Two more long presses: a catalog song in the search results opens the
catalog track's sheet (play, save, add to a playlist, the record, the
artist), and a row of the queue face opens the track's sheet with Play now,
Move to next and Remove from queue on top.`,
`More long presses: a catalog song in the search results opens the catalog
track's sheet (play, save, add to a playlist, the record, the artist); a
row of the queue face opens the track's sheet with Play now, Move to next
and Remove from queue on top; a raw result's file (and a file on the Browse
screen) opens its own — play from here, save this one file, browse; a name
in a room's list opens the peer sheet. The catalog record page's bar ⋯ is
the record's sheet: play, save, add every track to a playlist as streams,
the artist. Search corrects an obvious misspelling from the names this
phone knows (the library, the searches that worked) — the desktop's rule,
a word only when a real name is within two edits.`);
save();
console.log('round five wired');