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.
142 lines
6.7 KiB
JavaScript
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');
|