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

190 lines
10 KiB
JavaScript

// Round twenty-three: the rest of the lyrics tools — the per-track auto-sync
// switch, the tap-to-sync room (a full-screen layer the back gesture peels:
// the song plays, one tap per line, the taps are the timing), and the
// desktop's vocal-reduced copy. Store, room, sheet rows, mocks, walks, doc.
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');
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);
// ── the store: the switch, the reduced copy, the tap room ──
load('src/mobile/lyrics.svelte.js');
rep(`import { player } from "./player.svelte.js";`,
`import { player, current, playQueueAt } from "./player.svelte.js";
import { note } from "./notice.svelte.js";
import { openTiming, closeTiming } from "./nav.svelte.js";`);
rep(` searching: false,
});`,
` searching: false,
autoSyncOff: false, // the user turned the automatic correction off here
reducing: false, // a vocal-reduced copy is being made
reduced: null, // { from, to } once one has been made for this track
});`);
/* Only a READ carries the entry's auto-sync flag: the Rust's payload_from —
what lyrics_save_lines, lyrics_save_text and lyrics_select all return —
leaves that field at its default, so applying one of those over the store
would flip the switch back on while the backend still has it off. Reads
pass `full`; writes hand over the document and nothing else. */
rep(`function apply(p) {`, `function apply(p, full = false) {`);
rep(` lyr.autoOffset = p?.auto_offset_ms || 0;`,
` lyr.autoOffset = p?.auto_offset_ms || 0;
if (full) lyr.autoSyncOff = !!p?.auto_sync_off;`);
rep(` const p = await api.lyricsGet({ title: id.title, artist: id.artist, album: id.album, durationSecs: id.durationSecs, path: id.path, refresh: force ? true : null });
if (g !== gen) return;
apply(p);`,
` const p = await api.lyricsGet({ title: id.title, artist: id.artist, album: id.album, durationSecs: id.durationSecs, path: id.path, refresh: force ? true : null });
if (g !== gen) return;
apply(p, true);`);
/* `reduced` is deliberately NOT reset here: the swap to the vocal-reduced
copy changes the path, which IS this store's key, so a reset would forget
the way back the moment it was taken. */
rep(` Object.assign(lyr, { key: id.key, status: "loading", source: "", lines: [], plain: "", offset: 0, userOffset: 0, autoOffset: 0, candidates: null, searching: false });`,
` Object.assign(lyr, { key: id.key, status: "loading", source: "", lines: [], plain: "", offset: 0, userOffset: 0, autoOffset: 0, autoSyncOff: false, candidates: null, searching: false });`);
s = s.replace(/\s*$/, NL) + ins('ins-r23-lyrics-store.txt');
save();
// ── nav: one more back-able layer, peeled before the rest ──
load('src/mobile/nav.svelte.js');
rep(` viewerKey: null,`, ` viewerKey: null,
/* the tap-to-sync room — over everything, peeled first */
timing: false,`);
rep(` return (nav.viewer ? 1 : 0) + (nav.sheet ? 1 : 0) + (nav.half ? 1 : 0)`,
` return (nav.timing ? 1 : 0) + (nav.viewer ? 1 : 0) + (nav.sheet ? 1 : 0) + (nav.half ? 1 : 0)`);
rep(` depth = Math.max(0, depth - 1);
if (nav.viewer) nav.viewer = false;`,
` depth = Math.max(0, depth - 1);
if (nav.timing) nav.timing = false;
else if (nav.viewer) nav.viewer = false;`);
rep(`export function back() {
if (nav.viewer) nav.viewer = false;`,
`export function back() {
if (nav.timing) nav.timing = false;
else if (nav.viewer) nav.viewer = false;`);
rep(`export function closeViewer() {
if (!nav.viewer) return;
nav.viewer = false;
sync();
}`,
`export function closeViewer() {
if (!nav.viewer) return;
nav.viewer = false;
sync();
}
export function openTiming() {
if (nav.timing) return;
nav.timing = true;
sync();
}
export function closeTiming() {
if (!nav.timing) return;
nav.timing = false;
sync();
}`);
save();
// ── the room itself ──
fs.writeFileSync(root + 'src/mobile/SyncRoom.svelte', ins('ins-r23-syncroom.txt'));
// ── the shell hangs it over everything ──
load('src/mobile/Mobile.svelte');
rep(` import Viewer from "./Viewer.svelte";`, ` import Viewer from "./Viewer.svelte";
import SyncRoom from "./SyncRoom.svelte";`);
rep(` <Viewer />`, ` <Viewer />
<SyncRoom />`);
save();
// ── the lyrics sheet: the switch, the way into the room, the copy ──
load('src/mobile/HalfSheet.svelte');
rep(` import { lyr, nudge, searchCandidates, closeCandidates, chooseCandidate, forgetLyrics, pasteLyrics } from "./lyrics.svelte.js";`,
` import { lyr, nudge, searchCandidates, closeCandidates, chooseCandidate, forgetLyrics, pasteLyrics, setAutoSync, openTapSync, tapProgress, reduceVocals, backToOriginal } from "./lyrics.svelte.js";`);
rep(` const onThisAlbum = $derived((id) => top().n === "album" && top().c.id === id);`,
` /* the lyrics sheet: a tap-sync set left mid-tap for the playing track,
and whether a vocal-reduced copy is what is playing */
const tapLeft = $derived(kind === "lyrics" ? tapProgress() : null);
const canReduce = $derived(!!player.track?.path && !player.track?.stream);
const onReduced = $derived(!!lyr.reduced && player.track?.path === lyr.reduced.to);
const onThisAlbum = $derived((id) => top().n === "album" && top().c.id === id);`);
/* the sheet's first switch row — .row is a flex and .sw is flex: none, so it
lays out the way You's switch rows do */
rep(`{#snippet row(icon, t, m, fn)}
<button class="row" onclick={fn}>
<span class="lead"><Icon name={icon} size={20} /></span>
<span class="b"><span class="t">{t}</span>{#if m}<span class="m">{m}</span>{/if}</span>
</button>
{/snippet}`,
`{#snippet row(icon, t, m, fn)}
<button class="row" onclick={fn}>
<span class="lead"><Icon name={icon} size={20} /></span>
<span class="b"><span class="t">{t}</span>{#if m}<span class="m">{m}</span>{/if}</span>
</button>
{/snippet}` + NL + ins('ins-r23-halfsheet-swrow.txt').replace(/\s*$/, ''));
/* no act(): the sheet stays up, so the switch is SEEN to flip */
rep(` {@render row("skip-forward", "Lyrics later", "half a second", () => nudge(500))}`,
` {@render row("skip-forward", "Lyrics later", "half a second", () => nudge(500))}
{@render swrow("history", "Auto-sync this track", lyr.autoOffset ? \`correcting by \${lyr.autoOffset > 0 ? "+" : ""}\${(lyr.autoOffset / 1000).toFixed(1)} s\` : "let the app correct this track's timing", !lyr.autoSyncOff, () => { void setAutoSync(lyr.autoSyncOff); })}`);
{
const a = ` {@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); } }))}`;
rep(a, a + NL + ins('ins-r23-halfsheet-rows.txt').replace(/\s*$/, ''));
}
save();
// ── the room's own styles ──
load('src/mobile/shell.css');
s = s.replace(/\s*$/, NL) + ins('ins-r23-shell-css.txt');
save();
// ── mocks: the switch remembers, the taps come back timed, a copy is made ──
load('src/lib/mockLibrary.js');
rep(`/* every track has three timed lines; the sheet's tools have something to act on */`,
`/* the per-track auto-sync switch the lyrics sheet flips */
let autoSyncOff = false;
/* every track has three timed lines; the sheet's tools have something to act on */`);
rep(` plain: null, offset_ms: 0, auto_offset_ms: 0, auto_sync_off: false,`,
` plain: null, offset_ms: 0, auto_offset_ms: 0, auto_sync_off: autoSyncOff,`);
rep(` if (cmd === "lyrics_clear") return Promise.resolve();`,
ins('ins-r23-mock.txt') + ` if (cmd === "lyrics_clear") return Promise.resolve();`);
save();
// ── the walks ──
load('tests/mobile/smoke.mjs');
rep(` lyrLines: document.querySelectorAll('.lyr .ln').length,`,
` lyrLines: document.querySelectorAll('.lyr .ln').length,` + NL + ins('ins-r23-smoke-probe.txt').replace(/\s*$/, ''));
{
const a = " { do: `__click('.halfsheet .prompt-acts .btn-fill', 'Save')`, name: \"lyrics-pasted\", wait: 800 },";
rep(a, a + NL + ins('ins-r23-smoke-mock-steps.txt').replace(/\s*$/, ''));
}
{
const a = ` "lyrics-pasted": (i) => !i.half.includes("up") && i.lyrLines === 2,`;
rep(a, a + NL + ins('ins-r23-smoke-mock-expect.txt').replace(/\s*$/, ''));
}
{
const a = " { do: `(function () { var r = [].slice.call(document.querySelectorAll('.halfsheet .row')).find(function (x) { return /Lyrics earlier/.test(x.textContent); }); if (r) r.click(); })()`, wait: 700 },";
rep(a, a + NL + ins('ins-r23-smoke-phone-steps.txt').replace(/\s*$/, ''));
}
{
const a = ` "lyrics-nudged": (i) => i.half.includes("up") && !i.toast,`;
rep(a, a + NL + ins('ins-r23-smoke-phone-expect.txt').replace(/\s*$/, ''));
}
save();
// ── the doc ──
load('ANDROID.md');
rep(`or LRC with timestamps, kept for the track). One switch the desktop never needed:`,
`or LRC with timestamps, kept for the track).` + NL + ins('ins-r23-android.txt').replace(/\s*$/, '') + NL + `One switch the desktop never needed:`);
rep('`HalfSheet.svelte`, `Lane.svelte`, `Cover.svelte`, `Release.svelte` |',
'`HalfSheet.svelte`, `SyncRoom.svelte`, `Lane.svelte`, `Cover.svelte`, `Release.svelte` |');
rep('the waveform lane, the two card pieces', 'the tap-to-sync room, the waveform lane, the two card pieces');
save();
console.log('round 23 wired');