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

142 lines
6.7 KiB
JavaScript

// Round seven: the artwork viewer (the desktop's lightbox) from the
// now-playing art and a record page's cover, one layer the back gesture
// peels; and the doc's "not done yet" brought up to date.
const fs = require('fs');
const 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); };
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); };
// ── nav: the viewer is one more layer the back gesture peels ──
load('src/mobile/nav.svelte.js');
rep(` /* the half sheet: { kind, arg } */
half: null,
});`, ` /* the half sheet: { kind, arg } */
half: null,
/* the artwork viewer, and the art key it shows */
viewer: false,
viewerKey: null,
});`);
rep(`function backables() {
return (nav.sheet ? 1 : 0) + (nav.half ? 1 : 0)`, `function backables() {
return (nav.viewer ? 1 : 0) + (nav.sheet ? 1 : 0) + (nav.half ? 1 : 0)`);
rep(` depth = Math.max(0, depth - 1);
if (nav.sheet) nav.sheet = false;
else if (nav.half) nav.half = null;`, ` depth = Math.max(0, depth - 1);
if (nav.viewer) nav.viewer = false;
else if (nav.sheet) nav.sheet = false;
else if (nav.half) nav.half = null;`);
rep(`export function back() {
if (nav.sheet) nav.sheet = false;
else if (nav.half) nav.half = null;`, `export function back() {
if (nav.viewer) nav.viewer = false;
else if (nav.sheet) nav.sheet = false;
else if (nav.half) nav.half = null;`);
rep(`export function openHalf(kind, arg = null) {`, `export function openViewer(key) {
if (!key) return;
nav.viewerKey = key;
nav.viewer = true;
sync();
}
export function closeViewer() {
if (!nav.viewer) return;
nav.viewer = false;
sync();
}
export function openHalf(kind, arg = null) {`);
save();
// ── the shell renders it ──
load('src/mobile/Mobile.svelte');
rep(` import HalfSheet from "./HalfSheet.svelte";`, ` import HalfSheet from "./HalfSheet.svelte";
import Viewer from "./Viewer.svelte";`);
rep(` <HalfSheet />
<Sheet />`, ` <HalfSheet />
<Sheet />
<Viewer />`);
save();
// ── the now-playing art opens it ──
load('src/mobile/Sheet.svelte');
rep(` import { nav, closeSheet, morphSource, openHalf, push } from "./nav.svelte.js";`,
` import { nav, closeSheet, morphSource, openHalf, openViewer, push } from "./nav.svelte.js";`);
rep(` <div class="np-art" class:small>`, ` <div class="np-art" class:small onclick={() => openViewer(player.album?.art_path)} role="presentation">`);
save();
// ── a library record's cover opens it ──
load('src/mobile/screens/Album.svelte');
rep(` import { push, openHalf } from "../nav.svelte.js";`, ` import { push, openHalf, openViewer } from "../nav.svelte.js";`);
rep(` <div class="rec-cover"><Cover path={album.art_path} eager size={48} /></div>`,
` <div class="rec-cover" onclick={() => openViewer(album.art_path)} role="presentation"><Cover path={album.art_path} eager size={48} /></div>`);
save();
// ── CSS ──
load('src/mobile/shell.css');
s = s.replace(/\s*$/, NL) + `
/* ── the artwork viewer: the cover alone, over everything ─────────────── */
.viewer { position: fixed; inset: 0; z-index: 200; background: #000; display: grid; place-items: center; animation: viewer-in 160ms ease both; }
.viewer img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }
@keyframes viewer-in { from { opacity: 0; } to { opacity: 1; } }
`;
save();
// ── the walks ──
load('tests/mobile/smoke.mjs');
rep(` qrows: document.querySelectorAll('.q-face .row').length,`,
` qrows: document.querySelectorAll('.q-face .row').length,
viewer: !!document.querySelector('.viewer img'),`);
rep(` { do: \`document.querySelector('.mini').click()\`, name: "sheet", wait: 1100 },`,
` { do: \`document.querySelector('.mini').click()\`, name: "sheet", wait: 1100 },
/* the art opens the viewer; the back gesture closes it */
{ do: \`document.querySelector('.np-art').click()\`, name: "viewer", wait: 900 },
{ do: \`history.back()\`, name: "viewer-closed", wait: 700 },`);
afterLine(' album: (i) => i.screen.includes("tr-push") && !i.mini,', ` viewer: (i) => i.viewer,
"viewer-closed": (i) => !i.viewer && i.sheet.includes("up"),
`);
afterLine(' "album-playing": (i) => i.mini && !i.toast,', ` viewer: (i) => i.viewer,
"viewer-closed": (i) => !i.viewer && i.sheet.includes("up"),
`);
rep(` { do: \`document.querySelector('.mini').click()\`, name: "sheet", wait: 1400 },`,
` { do: \`document.querySelector('.mini').click()\`, name: "sheet", wait: 1400 },
{ do: \`document.querySelector('.np-art').click()\`, name: "viewer", wait: 1200 },
{ do: \`history.back()\`, name: "viewer-closed", wait: 800 },`);
save();
// ── the doc: what is not done, today ──
load('ANDROID.md');
rep(`## Not done yet
- Import (the desktop's import flow) on the phone.
- Save and Transfers exist (You → Transfers, fed by \`soulseek:download\`; a
live row cancels on tap; a long press on a catalog row offers the track's
own Save).
- Community's private layer (Spaces, holdings, the sealed inbox, friends by
key) — Freenet on the phone is a later experiment; the network side is in.
- Follows and the play history are per library folder, so the phone's own
folder starts empty of both; they arrive with a shared library.`,
`## Not done yet
- Community's private layer (Spaces, holdings, the sealed inbox, friends by
key, Who has it) — Freenet on the phone is a later experiment; the
network side is in. Rooms and chat are built and mock-walked; a live room
is Kyle's to join.
- The desktop-only doors and tools: the file and folder playlist imports,
the listener door and the lyric alignment models (a desktop runtime),
playlist export and the cover picker, drag reorder, catalog track
previews, the full discography page, the artist-artwork upscaler,
updates and the account section (sign-in lives on Community).
- Follows and the play history are per library folder, so the phone's own
folder starts empty of both; they arrive with a shared library.
- The artwork viewer shows the cover at full size; pinch zoom is not wired.`);
save();
console.log('round seven wired');