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.
199 lines
13 KiB
JavaScript
199 lines
13 KiB
JavaScript
// Round thirty-seven: dragged into place. The phone stood in for the
|
|
// desktop's drag with Move up / Move down / Move to the top on a row's
|
|
// sheet, which is the accessible path and stays — but a phone should drag
|
|
// BETTER than a desktop does, not worse. So: one gesture module for both
|
|
// lists, press and hold a row and it lifts, the rows below make room, let
|
|
// go and it lands. The hard parts are all about the finger being the thing
|
|
// that scrolls as well: a 320ms hold that does not fight the page's own
|
|
// scroll (touch-action cannot be flipped mid-gesture, so a non-passive
|
|
// touchmove refuses it instead), an edge pull measured in the scroller's
|
|
// CONTENT space, a drop that maps to an index the mutations already
|
|
// understand — moveItem and moveInQueue each grew a numeric target rather
|
|
// than a second path to the backend — and a drag refused outright while
|
|
// the filter is on, because a filtered list's visible order is not the
|
|
// stored order. Release without moving and the row's sheet opens exactly
|
|
// as it always did, which is what keeps the queue's sheet reachable at
|
|
// all. No Rust, no Kotlin, no new command and no new mock command.
|
|
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').replace(/\s+$/, '');
|
|
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 shell.css block is an APPEND with no anchor, so a second run would
|
|
write it twice where `once` cannot see it coming and would only throw
|
|
further down, leaving half a tree. Refuse one here, before anything is
|
|
touched: the gesture module is the file this round creates and nothing
|
|
else in the repo makes it. */
|
|
if (fs.existsSync(root + 'src/mobile/reorder.js')) {
|
|
throw new Error('src/mobile/reorder.js already exists — round 37 has already run against ' + root);
|
|
}
|
|
|
|
// ── the gesture, once, for both lists ─────────────────────────────────
|
|
fs.writeFileSync(root + 'src/mobile/reorder.js', ins('ins-r37-reorder.txt') + NL);
|
|
|
|
// ── the mutations widen; they are not replaced ────────────────────────
|
|
load('src/mobile/playlists.svelte.js');
|
|
rep(` /** the last removal: { playlistId, rows: [{ item, position }], label } */`,
|
|
` /** true while a row is in hand on the playlist page: the page's own
|
|
* re-read waits for the drop rather than replacing the rows under it */
|
|
dragging: false,
|
|
/** the last removal: { playlistId, rows: [{ item, position }], label } */`);
|
|
rep(`/* ── reorder: a row up, down or to the top; the complete id order goes
|
|
in one call, as the desktop's drop does ── */`,
|
|
`/* ── reorder: a row up, down, to the top, or to whatever index a drag
|
|
dropped it at; the complete id order goes in one call either way, as the
|
|
desktop's drop does ── */`);
|
|
rep(` const to = where === "top" ? 0 : where === "up" ? from - 1 : from + 1;`,
|
|
` const to = typeof where === "number" ? Math.max(0, Math.min(ids.length - 1, where)) : where === "top" ? 0 : where === "up" ? from - 1 : from + 1;`);
|
|
save();
|
|
|
|
load('src/mobile/player.svelte.js');
|
|
rep(`/** An upcoming row one place up or down, never past the current track. */`,
|
|
`/** An upcoming row one place up or down — or to an absolute index, which
|
|
* is what a drop on the queue face hands it — never past the current
|
|
* track. The guards were already the right guards for either. */`);
|
|
rep(` const j = i + (dir === "up" ? -1 : 1);`,
|
|
` const j = typeof dir === "number" ? dir : i + (dir === "up" ? -1 : 1);`);
|
|
save();
|
|
|
|
// ── the playlist page ─────────────────────────────────────────────────
|
|
load('src/mobile/screens/Playlist.svelte');
|
|
/* a sibling import rather than a rewritten one: the util.js line is the
|
|
exact class of line two rounds of an earlier wave collided on */
|
|
rep(` import { mmss, longDur, stamp } from "../util.js";`,
|
|
` import { mmss, longDur, stamp } from "../util.js";
|
|
import { reorder } from "../reorder.js";`);
|
|
rep(`rowClick } from "../playlists.svelte.js";`, `rowClick, moveItem } from "../playlists.svelte.js";`);
|
|
rep(` function tap(item) { if (!rowClick(pl, items, item)) sheet(item); }`,
|
|
` function tap(item) { if (!rowClick(pl, items, item)) sheet(item); }` + NL + ins('ins-r37-pl-glue.txt'));
|
|
rep(` void again;`,
|
|
` void again;
|
|
/* a refetch that lands mid-drag would yank the rows out from under
|
|
the gesture; the desktop defers one the same way
|
|
(skippedWhileDrag). Reading the flag is what makes this effect
|
|
re-run by itself the moment the drag has finished. */
|
|
if (pls.dragging) return;`);
|
|
rep(` {#if q}<button class="iconbtn sm" onclick={() => (q = "")} aria-label="Clear"><Icon name="x" size={18} /></button>{/if}
|
|
</div>
|
|
{/if}`,
|
|
` {#if q}<button class="iconbtn sm" onclick={() => (q = "")} aria-label="Clear"><Icon name="x" size={18} /></button>{/if}
|
|
</div>
|
|
{#if q && items.length > 1}<div class="pl-note">Clear the search to drag rows into place.</div>{/if}
|
|
{/if}`);
|
|
rep(` <div class="tracks pl-rows">`, ` <div class="tracks pl-rows" use:reorder={rowDrag}>`);
|
|
save();
|
|
|
|
// ── the queue face ────────────────────────────────────────────────────
|
|
load('src/mobile/Sheet.svelte');
|
|
rep(` import { mmss, drag } from "./util.js";`,
|
|
` import { mmss, drag } from "./util.js";
|
|
import { reorder } from "./reorder.js";`);
|
|
rep(`cycleRepeat } from "./player.svelte.js";`, `cycleRepeat, moveInQueue } from "./player.svelte.js";`);
|
|
rep(` function lineSeek(l) { seekSecs(Math.max(0, (l.time_ms + lyr.offset) / 1000)); }`,
|
|
` function lineSeek(l) { seekSecs(Math.max(0, (l.time_ms + lyr.offset) / 1000)); }` + NL + ins('ins-r37-sheet-glue.txt'));
|
|
rep(` <div class="q-face">`, ` <div class="q-face" use:reorder={queueDrag}>`);
|
|
save();
|
|
|
|
// ── the sheet teaches the gesture where someone is already looking for
|
|
// reordering. No new chrome, no invented grip. ────────────────────────
|
|
load('src/mobile/HalfSheet.svelte');
|
|
rep(`{@render row("chevron-up", "Move up", "", act(() => moveInQueue(trackArg.queueIndex, "up")))}`,
|
|
`{@render row("chevron-up", "Move up", "or press and hold a row to drag it", act(() => moveInQueue(trackArg.queueIndex, "up")))}`);
|
|
rep(`{@render row("chevron-up", "Move up", "", act(() => moveItem(p, rows, item, "up").catch(oops)))}`,
|
|
`{@render row("chevron-up", "Move up", "or press and hold a row to drag it", act(() => moveItem(p, rows, item, "up").catch(oops)))}`);
|
|
save();
|
|
|
|
// ── the look: appended, so the guard above is what refuses a second run
|
|
load('src/mobile/shell.css');
|
|
s = s.replace(/\s*$/, NL) + NL + ins('ins-r37-css.txt') + NL;
|
|
save();
|
|
|
|
// ── the walks ─────────────────────────────────────────────────────────
|
|
load('tests/mobile/smoke.mjs');
|
|
/* A row lifting under a finger buzzes. Chrome refuses navigator.vibrate until
|
|
the frame has had a real tap, and every pointer event this walk sends is
|
|
synthetic — so the lift logs an intervention notice at error level, which
|
|
the console trap would otherwise count as a problem in a step that is
|
|
working perfectly. It is a fact about the harness, not about the app, and
|
|
it joins the reload's orphaned-callback notice on the same short list. */
|
|
rep(` cdp.on["Log.entryAdded"] = (p) => { if (p.entry.level === "error") problems.push("LOG " + p.entry.text + " " + (p.entry.url || "")); };`,
|
|
` cdp.on["Log.entryAdded"] = (p) => {
|
|
if (p.entry.level !== "error") return;
|
|
if (/navigator\\.vibrate/.test(p.entry.text || "")) return;
|
|
problems.push("LOG " + p.entry.text + " " + (p.entry.url || ""));
|
|
};`);
|
|
rep(`window.__settle = function () {`, ins('ins-r37-helpers.txt') + NL + `window.__settle = function () {`);
|
|
rep(` plthumbs: document.querySelectorAll('.pl-rows .thumb img').length,`,
|
|
` plthumbs: document.querySelectorAll('.pl-rows .thumb img').length,` + NL + ins('ins-r37-probe.txt'));
|
|
rep(` { do: \`__click('.halfsheet .row', 'Move down')\`, name: "playlist-moved-down", wait: 900 },`,
|
|
` { do: \`__click('.halfsheet .row', 'Move down')\`, name: "playlist-moved-down", wait: 900 },` + NL + ins('ins-r37-mock-steps.txt'));
|
|
rep(` { do: \`__click('.halfsheet .row', 'Move up')\`, name: "queue-moved-up", wait: 600 },`,
|
|
` { do: \`__click('.halfsheet .row', 'Move up')\`, name: "queue-moved-up", wait: 600 },` + NL + ins('ins-r37-mock-qsteps.txt'));
|
|
rep(` "playlist-moved-down": (i) => i.plfirst.startsWith("Track 1"),`,
|
|
` "playlist-moved-down": (i) => i.plfirst.startsWith("Track 1"),` + NL + ins('ins-r37-mock-expect.txt'));
|
|
rep(` { do: \`__click('.plgrid .release', 'Walk test')\`, name: "playlist-filled", wait: 1500 },`,
|
|
` { do: \`__click('.plgrid .release', 'Walk test')\`, name: "playlist-filled", wait: 1500 },` + NL + ins('ins-r37-phone-plsteps.txt'));
|
|
rep(` { do: \`document.querySelectorAll('.fbtn')[2].click()\`, name: "queue", wait: 800 },`,
|
|
` { do: \`document.querySelectorAll('.fbtn')[2].click()\`, name: "queue", wait: 800 },` + NL + ins('ins-r37-phone-qsteps.txt'));
|
|
rep(` "playlist-filled": (i) => i.title.startsWith("Walk test") && i.plrows >= 1,`,
|
|
` "playlist-filled": (i) => i.title.startsWith("Walk test") && i.plrows >= 1,` + NL + ins('ins-r37-phone-expect.txt'));
|
|
save();
|
|
|
|
// ── the doc ───────────────────────────────────────────────────────────
|
|
load('ANDROID.md');
|
|
rep(` Remove from playlist; Move up, Move down and Move to the top reorder
|
|
without a drag (\`playlist_set_order\`, the whole order in one call, as the
|
|
desktop's drop does).`,
|
|
` Remove from playlist; Move up, Move down and Move to the top reorder from
|
|
the sheet (\`playlist_set_order\`, the whole order in one call, as the
|
|
desktop's drop does) — the accessible path, and the one that stays honest
|
|
while the filter is on.`);
|
|
rep(` A removal is undoable for seven seconds from the neutral toast
|
|
(\`notice.svelte.js\` — the desktop's undo pill); the stash pins the
|
|
playlist it came from.`,
|
|
` A removal is undoable for seven seconds from the neutral toast
|
|
(\`notice.svelte.js\` — the desktop's undo pill); the stash pins the
|
|
playlist it came from.
|
|
` + ins('ins-r37-doc-drag.txt'));
|
|
rep(`yt-dlp, a desktop tool), drag reorder, and Share on SoulStream.`,
|
|
`yt-dlp, a desktop tool), and Share on SoulStream.`);
|
|
rep(`shelf and a page with every row kind (a local row, the stream row through
|
|
the resolver, the filter, a removal and its undo, unpin, rename, a new
|
|
playlist, Add to playlist from a record's sheet, delete) → the import doors`,
|
|
`shelf and a page with every row kind (a local row, the stream row through
|
|
the resolver, the filter, a removal and its undo, a row dragged into place
|
|
and dragged back, a drag refused while the filter is on, a press released
|
|
without moving still opening the row's sheet, a drop the backend rejects
|
|
snapping back, unpin, rename, a new playlist, Add to playlist from a
|
|
record's sheet, delete) → the import doors`);
|
|
rep(`library) → the artwork viewer → a queue row's sheet → a catalog song's,`,
|
|
`library) → the artwork viewer → a queue row's sheet and a queue row
|
|
dragged into place → a catalog song's,`);
|
|
/* the weather block's count is a running total across the rounds that add to
|
|
it: round thirty-one wrote nine, round thirty-five took it to twelve with
|
|
the export's three, and this round's two make fourteen */
|
|
rep(`Twelve mock steps drive them, and the phone walk carries`,
|
|
`Fourteen mock steps drive them, and the phone walk carries`);
|
|
rep(`- **Still desktop-only, smaller.** Drag reorder (Move up / down / to the top
|
|
stand in), catalog track previews, the full discography page, and the
|
|
fused-artist portrait verdict — that last one needs the identity merge the
|
|
phone does not run, so porting it means porting the merge first.`,
|
|
`- **Still desktop-only, smaller.** Catalog track previews, the full
|
|
discography page, and the fused-artist portrait verdict — that last one
|
|
needs the identity merge the phone does not run, so porting it means
|
|
porting the merge first.`);
|
|
save();
|
|
|
|
console.log('round 37 wired');
|