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,141 @@
|
||||
// Round thirteen: reordering without a drag — a playlist row moved up, down
|
||||
// or to the top from its sheet (playlist_set_order, the whole order in one
|
||||
// call, as the desktop's drop does), and a queue row moved up or down.
|
||||
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); };
|
||||
|
||||
// ── playlists: a row moved, the whole order persisted ──
|
||||
load('src/mobile/playlists.svelte.js');
|
||||
rep(`/* ── removal, undoable ───────────────────────────────────────────────── */`,
|
||||
`/* ── reorder: a row up, down or to the top; the complete id order goes
|
||||
in one call, as the desktop's drop does ── */
|
||||
export async function moveItem(p, items, item, where) {
|
||||
const ids = items.map((i) => i.id);
|
||||
const from = ids.indexOf(item.id);
|
||||
if (from < 0) return false;
|
||||
const to = where === "top" ? 0 : where === "up" ? from - 1 : from + 1;
|
||||
if (to < 0 || to >= ids.length || to === from) return false;
|
||||
ids.splice(from, 1);
|
||||
ids.splice(to, 0, item.id);
|
||||
await api.playlistSetOrder(p.id, ids);
|
||||
await refreshPlaylists();
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── removal, undoable ───────────────────────────────────────────────── */`);
|
||||
save();
|
||||
|
||||
// ── the player: a queue row up or down ──
|
||||
load('src/mobile/player.svelte.js');
|
||||
rep(`/** Pull an upcoming row up to right after the current track. */
|
||||
export function moveToNext(i) {`,
|
||||
`/** An upcoming row one place up or down, never past the current track. */
|
||||
export function moveInQueue(i, dir) {
|
||||
const j = i + (dir === "up" ? -1 : 1);
|
||||
if (i <= player.qi || j <= player.qi || i >= player.queue.length || j >= player.queue.length) return;
|
||||
const [row] = player.queue.splice(i, 1);
|
||||
player.queue.splice(j, 0, row);
|
||||
armPreload();
|
||||
writeSession();
|
||||
}
|
||||
/** Pull an upcoming row up to right after the current track. */
|
||||
export function moveToNext(i) {`);
|
||||
save();
|
||||
|
||||
// ── the sheets ──
|
||||
load('src/mobile/HalfSheet.svelte');
|
||||
rep(` import { player, playNext, enqueue, playEntries, playQueueAt, removeFromQueue, moveToNext, setSleep, sleepLabel } from "./player.svelte.js";`,
|
||||
` import { player, playNext, enqueue, playEntries, playQueueAt, removeFromQueue, moveToNext, moveInQueue, setSleep, sleepLabel } from "./player.svelte.js";`);
|
||||
rep(` import { pls, bulk, bulkable, runBulk, coverFor, queueable, entryOf, playFrom, shufflePlay, togglePin, renamePlaylist, describePlaylist, duplicatePlaylist, deletePlaylist, removeItems, addEntries, createPlaylist } from "./playlists.svelte.js";`,
|
||||
` import { pls, bulk, bulkable, runBulk, coverFor, queueable, entryOf, playFrom, shufflePlay, togglePin, renamePlaylist, describePlaylist, duplicatePlaylist, deletePlaylist, removeItems, addEntries, createPlaylist, moveItem } from "./playlists.svelte.js";`);
|
||||
// the queue row: up / down beside Move to next
|
||||
rep(` {#if trackArg.queueIndex > player.qi + 1}
|
||||
{@render row("skip-forward", "Move to next", "right after this one", act(() => moveToNext(trackArg.queueIndex)))}
|
||||
{/if}`,
|
||||
` {#if trackArg.queueIndex > player.qi + 1}
|
||||
{@render row("skip-forward", "Move to next", "right after this one", act(() => moveToNext(trackArg.queueIndex)))}
|
||||
{@render row("chevron-up", "Move up", "", act(() => moveInQueue(trackArg.queueIndex, "up")))}
|
||||
{/if}
|
||||
{#if trackArg.queueIndex < player.queue.length - 1}
|
||||
{@render row("chevron-down", "Move down", "", act(() => moveInQueue(trackArg.queueIndex, "down")))}
|
||||
{/if}`);
|
||||
// the playlist row: up / down / top before Remove
|
||||
rep(` <div class="sheet-hair"></div>
|
||||
{@render row("x", "Remove from playlist", "undo for a few seconds after", act(() => removeItems(p, rows, [item]).catch(oops)))}`,
|
||||
` {#if rows.length > 1}
|
||||
{@const at = rows.findIndex((r) => r.id === item.id)}
|
||||
<div class="sheet-hair"></div>
|
||||
{#if at > 0}{@render row("chevron-up", "Move up", "", act(() => moveItem(p, rows, item, "up").catch(oops)))}{/if}
|
||||
{#if at < rows.length - 1}{@render row("chevron-down", "Move down", "", act(() => moveItem(p, rows, item, "down").catch(oops)))}{/if}
|
||||
{#if at > 1}{@render row("skip-back", "Move to the top", "", act(() => moveItem(p, rows, item, "top").catch(oops)))}{/if}
|
||||
{/if}
|
||||
<div class="sheet-hair"></div>
|
||||
{@render row("x", "Remove from playlist", "undo for a few seconds after", act(() => removeItems(p, rows, [item]).catch(oops)))}`);
|
||||
save();
|
||||
|
||||
// ── mocks ──
|
||||
load('src/lib/mockLibrary.js');
|
||||
rep(` if (cmd === "playlist_relink_item") {`,
|
||||
` if (cmd === "playlist_set_order") {
|
||||
const p = plOf(args);
|
||||
if (!p) return Promise.reject(new Error("playlist_set_order: no such playlist"));
|
||||
const by = new Map((PL_ITEMS[p.id] || []).map((x) => [x.id, x]));
|
||||
const next = (args?.orderedIds || []).map(Number).map((id) => by.get(id)).filter(Boolean);
|
||||
for (const x of PL_ITEMS[p.id] || []) if (!next.includes(x)) next.push(x);
|
||||
PL_ITEMS[p.id] = next;
|
||||
next.forEach((x, i) => { x.position = i; });
|
||||
plTouch(p);
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (cmd === "playlist_relink_item") {`);
|
||||
save();
|
||||
|
||||
// ── the walks ──
|
||||
load('tests/mobile/smoke.mjs');
|
||||
rep(` plfilter: !!document.querySelector('.plfilter input'),`,
|
||||
` plfilter: !!document.querySelector('.plfilter input'),
|
||||
plfirst: (document.querySelector('.pl-rows .trk .t') || {}).textContent || '',
|
||||
qfirst: (document.querySelector('.q-face .row .t') || {}).textContent || '',`);
|
||||
rep(` { do: \`__click('.toast.notice .na', 'Undo')\`, name: "playlist-undone", wait: 1000 },`,
|
||||
` { do: \`__click('.toast.notice .na', 'Undo')\`, name: "playlist-undone", wait: 1000 },
|
||||
/* reordered from the sheet: the second row up, then back down */
|
||||
{ do: \`document.querySelectorAll('.pl-rows .trk')[1].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))\`, wait: 600 },
|
||||
{ do: \`__click('.halfsheet .row', 'Move up')\`, name: "playlist-moved-up", wait: 900 },
|
||||
{ do: \`document.querySelectorAll('.pl-rows .trk')[0].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))\`, wait: 600 },
|
||||
{ do: \`__click('.halfsheet .row', 'Move down')\`, name: "playlist-moved-down", wait: 900 },`);
|
||||
rep(` "playlist-undone": (i) => i.plrows === 6 && !i.notice,`,
|
||||
` "playlist-undone": (i) => i.plrows === 6 && !i.notice,
|
||||
"playlist-moved-up": (i) => i.plfirst.startsWith("Track 2"),
|
||||
"playlist-moved-down": (i) => i.plfirst.startsWith("Track 1"),`);
|
||||
rep(` { do: \`__click('.halfsheet .row', 'Remove from queue')\`, name: "queue-removed", wait: 600 },`,
|
||||
` { do: \`__click('.halfsheet .row', 'Remove from queue')\`, name: "queue-removed", wait: 600 },
|
||||
/* the second upcoming row moved up */
|
||||
{ do: \`(function () { globalThis.__q2 = document.querySelectorAll('.q-face .row')[1].querySelector('.t').textContent; document.querySelectorAll('.q-face .row')[1].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true })); })()\`, wait: 600 },
|
||||
{ do: \`__click('.halfsheet .row', 'Move up')\`, name: "queue-moved-up", wait: 600 },`);
|
||||
rep(` "queue-removed": (i) => !i.half.includes("up") && i.qrows === globalThis.__qrows - 1,`,
|
||||
` "queue-removed": (i) => !i.half.includes("up") && i.qrows === globalThis.__qrows - 1,
|
||||
"queue-moved-up": (i) => !i.half.includes("up") && !!i.qfirst && i.qrows === globalThis.__qrows - 1,`);
|
||||
save();
|
||||
|
||||
// ── the doc ──
|
||||
load('ANDROID.md');
|
||||
rep(` Remove from playlist.`, ` 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).`);
|
||||
rep(`row of the queue face opens the track's sheet with Play now, Move to next
|
||||
and Remove from queue on top;`, `row of the queue face opens the track's sheet with Play now, Move to next,
|
||||
Move up, Move down and Remove from queue on top;`);
|
||||
save();
|
||||
console.log('round thirteen wired');
|
||||
Reference in new issue
Block a user