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,189 @@
|
||||
// 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');
|
||||
Reference in new issue
Block a user