// 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(` `, ` `); 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(`
`, `
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(`
`, `
openViewer(album.art_path)} role="presentation">
`); 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');