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.
195 lines
11 KiB
JavaScript
195 lines
11 KiB
JavaScript
// Round twenty: a peer in full — the peer sheet's "About this user" opens a
|
|
// screen of its own (screens/Peer.svelte): the card their client serves
|
|
// (slsk_user_info), the interests they declared (slsk_user_interests), and
|
|
// the comparison this phone makes of their share against the library
|
|
// (overlap.rs), whose gap rows are the point of opening a stranger.
|
|
const fs = require('fs');
|
|
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
|
|
const INS = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/';
|
|
const NL = String.fromCharCode(10);
|
|
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); };
|
|
|
|
/* Two of the anchors below are re-emitted — round twenty-one wants the
|
|
sheet's Browse block byte-identical — so a second run would double-insert
|
|
where `once` cannot see it. Refuse one before anything is written. */
|
|
if (fs.existsSync(root + 'src/mobile/screens/Peer.svelte')) {
|
|
throw new Error('round twenty is already applied in ' + root + ' — reset the tree rather than re-running');
|
|
}
|
|
|
|
// ── the screen ──
|
|
fs.writeFileSync(root + 'src/mobile/screens/Peer.svelte', fs.readFileSync(INS + 'ins-r20-peer.txt', 'utf8'));
|
|
|
|
// ── the door: one row on the peer sheet, above Browse ──
|
|
load('src/mobile/HalfSheet.svelte');
|
|
{
|
|
const a = ` {#if !(top().n === "browse" && top().c.username === u)}
|
|
{@render row("folder-open", "Browse their files", "everything they share", act(() => push("browse", { username: u })))}
|
|
{/if}`;
|
|
rep(a, ` {#if !(top().n === "peer" && top().c.username === u)}
|
|
{@render row("users", "About this user", "their card, and what they have that you don't", act(() => push("peer", { username: u })))}
|
|
{/if}
|
|
` + a);
|
|
}
|
|
save();
|
|
|
|
// ── the route ──
|
|
load('src/mobile/Mobile.svelte');
|
|
rep(` import Browse from "./screens/Browse.svelte";`, ` import Browse from "./screens/Browse.svelte";
|
|
import Peer from "./screens/Peer.svelte";`);
|
|
rep(` case "browse": return { title: e.c.username, kicker: "Their files" };`,
|
|
` case "browse": return { title: e.c.username, kicker: "Their files" };
|
|
case "peer": return { title: e.c.username, kicker: "On Soulseek" };`);
|
|
rep(` {:else if entry.n === "browse"}
|
|
<Browse username={entry.c.username} />`,
|
|
` {:else if entry.n === "browse"}
|
|
<Browse username={entry.c.username} />
|
|
{:else if entry.n === "peer"}
|
|
<Peer username={entry.c.username} {screenEl} />`);
|
|
save();
|
|
|
|
// ── the viewer takes a data: key, so their own picture opens like a cover ──
|
|
load('src/mobile/Viewer.svelte');
|
|
rep(` if (/^https?:/i.test(k)) { full = k; return; }`,
|
|
` if (/^https?:/i.test(k)) { full = k; return; }
|
|
if (/^data:/i.test(k)) { full = k; return; } /* a peer's own picture: the bytes are already here */`);
|
|
save();
|
|
|
|
// ── the look ──
|
|
load('src/mobile/shell.css');
|
|
s = s.replace(/\s*$/, NL) + `
|
|
/* A peer in full: the head wears their own picture, their own words sit in
|
|
a card, and the comparison's rows read as records they have and you don't */
|
|
.pr-head .pr-pic { width: 44px; height: 44px; flex: none; padding: 0; border: 0; border-radius: 13px; overflow: hidden; background: var(--surface); box-shadow: var(--ring); }
|
|
.pr-head .pr-pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
.pr-about { margin: 2px var(--pad) 10px; padding: 12px 14px; border-radius: var(--r-card); background: var(--surface); box-shadow: var(--ring); font-size: 13.5px; line-height: 1.55; color: var(--text-secondary); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
.chips.pr-tags { flex-wrap: wrap; overflow: visible; row-gap: 7px; }
|
|
.pr-gap .lead { color: var(--text-muted); }
|
|
`;
|
|
save();
|
|
|
|
// ── the mocks: a card, a picture, interests, and a ledger per username ──
|
|
load('src/lib/mockLibrary.js');
|
|
rep(`const TITLES = ["One More Time"`, `/* a peer's own card, and the comparison overlap.rs keeps per username */
|
|
const MOCK_PIC = "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAEUlEQVR42mOQz+7HihiGlgQAV11GQceAU4sAAAAASUVORK5CYII=";
|
|
const MOCK_ABOUT = "Ripping my own vinyl since 2004. FLAC only, folders named Artist - Year - Album. Ask before you queue a whole discography.";
|
|
const MOCK_LIKES = ["french house", "vinyl rips", "detroit techno", "boards of canada"];
|
|
const MOCK_HATES = ["transcodes", "128 kbps"];
|
|
const OVERLAP_COMMON = [["Daft Punk", "Discovery"], ["Daft Punk", "Homework"], ["Caribou", "Swim"]];
|
|
const OVERLAP_GAPS = [
|
|
["Boards of Canada", "Geogaddi"],
|
|
["Aphex Twin", "Drukqs"],
|
|
["Daft Punk", "Alive 2007"],
|
|
["Burial", "Untrue"],
|
|
["Four Tet", "Rounds"],
|
|
["Autechre", "Amber"],
|
|
];
|
|
const overlaps = new Map();
|
|
const pairsOf = (list) => list.map(([artist, album]) => ({ artist, album }));
|
|
const TITLES = ["One More Time"`);
|
|
rep(` if (cmd === "start_search") {`, ` if (cmd === "slsk_user_info") {
|
|
const u = String(args?.username || "");
|
|
const peer = PEERS.find((x) => x.username === u);
|
|
return new Promise((r) => setTimeout(() => r({
|
|
username: u,
|
|
description: MOCK_ABOUT,
|
|
picture_png_base64: MOCK_PIC,
|
|
picture_note: null,
|
|
total_uploads: 18422,
|
|
queue_size: peer ? peer.queue : 0,
|
|
has_free_slot: peer ? peer.slot : true,
|
|
}), 450));
|
|
}
|
|
if (cmd === "slsk_user_interests") {
|
|
return new Promise((r) => setTimeout(() => r({ likes: MOCK_LIKES.slice(), hates: MOCK_HATES.slice() }), 400));
|
|
}
|
|
if (cmd === "overlap_list") return Promise.resolve([...overlaps.values()]);
|
|
if (cmd === "overlap_compute") {
|
|
/* the real one browses their whole share before it compares: the wait is the point */
|
|
const u = String(args?.username || "");
|
|
const o = {
|
|
username: u,
|
|
computed_at: Math.floor(Date.now() / 1000),
|
|
folders: 812,
|
|
audio_files: 9134,
|
|
their_albums: 74,
|
|
albums_in_common: pairsOf(OVERLAP_COMMON),
|
|
albums_in_common_count: OVERLAP_COMMON.length,
|
|
artists_in_common: ["Daft Punk", "Caribou"],
|
|
artists_in_common_count: 2,
|
|
gaps: pairsOf(OVERLAP_GAPS),
|
|
gaps_count: OVERLAP_GAPS.length,
|
|
score: OVERLAP_COMMON.length / 74,
|
|
};
|
|
return new Promise((r) => setTimeout(() => { overlaps.set(u.toLowerCase(), o); r(o); }, 1100));
|
|
}
|
|
if (cmd === "overlap_forget") { overlaps.delete(String(args?.username || "").toLowerCase()); return Promise.resolve(); }
|
|
if (cmd === "start_search") {`);
|
|
save();
|
|
|
|
// ── the walk ──
|
|
load('tests/mobile/smoke.mjs');
|
|
rep(` brwHead: (document.querySelector('.brw-head .m') || {}).textContent || '',`,
|
|
` brwHead: (document.querySelector('.brw-head .m') || {}).textContent || '',
|
|
peerLine: (document.querySelector('.pr-head .m') || {}).textContent || '',
|
|
peerDesc: (document.querySelector('.pr-about') || {}).textContent || '',
|
|
peerPic: !!document.querySelector('.pr-pic img'),
|
|
peerTags: document.querySelectorAll('.pr-tags .chip').length,
|
|
peerGaps: document.querySelectorAll('.pr-gap').length,
|
|
peerSum: (document.querySelector('.pr-sum') || {}).textContent || '',`);
|
|
{
|
|
const a = ` { do: \`document.querySelector('.peer-head .peer-menu').click()\`, name: "peer-sheet", wait: 700 },
|
|
{ do: \`__click('.halfsheet .row', 'Browse their files')\`, name: "browse", wait: 1400 },`;
|
|
rep(a, ` { do: \`document.querySelector('.peer-head .peer-menu').click()\`, name: "peer-sheet", wait: 700 },
|
|
/* the stranger in full: the card their client serves, their picture in
|
|
the viewer, what they declared, and the comparison of their share
|
|
against this library — a record they have and it doesn't, found */
|
|
{ do: \`__click('.halfsheet .row', 'About this user')\`, name: "peer-screen", wait: 1400 },
|
|
{ do: \`document.querySelector('.pr-pic').click()\`, name: "peer-picture", wait: 900 },
|
|
{ do: \`history.back()\`, wait: 700 },
|
|
{ do: \`__click('.screen .btn', 'Compare with my library')\`, name: "peer-compare", wait: 2200 },
|
|
{ do: \`__click('.pr-gap', 'Geogaddi')\`, name: "peer-gap-open", wait: 1300 },
|
|
{ do: \`history.back()\`, name: "peer-again", wait: 1200 },
|
|
{ do: \`__click('.screen .btn', 'Forget this comparison')\`, name: "peer-forgotten", wait: 1100 },
|
|
{ do: \`document.querySelector('.pr-head .iconbtn').click()\`, name: "peer-sheet-again", wait: 700 },
|
|
{ do: \`__click('.halfsheet .row', 'Browse their files')\`, name: "browse", wait: 1400 },`);
|
|
}
|
|
rep(` "peer-sheet": (i) => i.half.includes("up") && /Browse their files/.test(i.halfText) && /Play this folder/.test(i.halfText),`,
|
|
` "peer-sheet": (i) => i.half.includes("up") && /Browse their files/.test(i.halfText) && /Play this folder/.test(i.halfText),
|
|
"peer-screen": (i) => i.title.startsWith("hifi_hank") && /Ripping my own vinyl/.test(i.peerDesc) && i.peerPic && i.peerTags === 6 && /slot is free/.test(i.peerLine) && /served/.test(i.peerLine) && i.peerGaps === 0,
|
|
"peer-picture": (i) => i.viewer,
|
|
"peer-compare": (i) => i.peerGaps === 6 && /in your library/.test(i.peerSum),
|
|
"peer-gap-open": (i) => i.title.startsWith("Search") && i.sq === "Boards of Canada Geogaddi",
|
|
"peer-again": (i) => i.title.startsWith("hifi_hank") && i.peerGaps === 6,
|
|
"peer-forgotten": (i) => i.peerGaps === 0 && !i.peerSum && /forgotten/i.test(i.notice),
|
|
"peer-sheet-again": (i) => i.half.includes("up") && /Browse their files/.test(i.halfText) && !/About this user/.test(i.halfText),`);
|
|
save();
|
|
|
|
// ── the doc ──
|
|
load('ANDROID.md');
|
|
rep(`## Playlists`, `Tapping **About this user** on the peer sheet opens the stranger in full
|
|
(\`screens/Peer.svelte\`, \`push("peer", { username })\`): the card their own
|
|
client serves (\`slsk_user_info\` — what they wrote about themselves, their
|
|
picture, whether a slot is free, the queue, how much they have served),
|
|
the likes and hates they declared to the server (\`slsk_user_interests\`),
|
|
and the comparison this phone makes of their share against the library
|
|
(\`overlap.rs\`: \`overlap_compute\` browses them on purpose, \`overlap_list\`
|
|
reads what was compared before, \`overlap_forget\` drops it). Nothing is
|
|
uploaded to work it out — their folder names against your albums, here.
|
|
The comparison's rows are the reason to open a stranger: every record they
|
|
have by an artist in your library that you don't, each one opening the
|
|
catalog search that finds it. Their picture opens in the artwork viewer,
|
|
which takes a \`data:\` key now as well as a library path and an https URL.
|
|
|
|
## Playlists`);
|
|
save();
|
|
console.log('round twenty wired');
|