// Round eighteen: the hub's raw-search filters on the phone's network // results (free slot only; FLAC or MP3 320+), and the lyrics door the // desktop has for pasted words (plain text, or LRC with timestamps). const fs = require('fs'); const root = process.env.SS_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); }; // ── Search: the filters ── load('src/mobile/screens/Search.svelte'); rep(` function playPeer(g, i) {`, ` /* the hub's filters over the raw results: a free slot only, a format */ let netFree = $state(false); let netFmt = $state("any"); const shownGroups = $derived(net.groups.filter((g) => { if (netFree && !g.slot) return false; if (netFmt === "flac") return g.topExt === "flac"; if (netFmt === "mp3320") return g.topExt !== "flac" && (g.maxBitrate ?? 0) >= 320; return true; })); function playPeer(g, i) {`); rep(` {#if net.error}
{net.error}
{/if}`, ` {#if net.error}
{net.error}
{/if} {#if net.groups.length}
{#each [["any", "Any format"], ["flac", "FLAC"], ["mp3320", "MP3 320+"]] as [v, l] (v)} {/each}
{#if shownGroups.length !== net.groups.length}
{shownGroups.length} of {net.groups.length} folders
{/if} {/if}`); rep(` {#each net.groups.slice(0, 40) as g (g.key)}`, ` {#each shownGroups.slice(0, 40) as g (g.key)}`); save(); // ── lyrics: pasted words ── load('src/mobile/lyrics.svelte.js'); s = s.replace(/\s*$/, String.fromCharCode(10)) + ` /** Pasted words: LRC with timestamps becomes synced, anything else plain. */ export async function pasteLyrics(text) { const id = identity(); const t = String(text || "").trim(); if (!id || !t) return false; try { const p = await api.lyricsSaveText(id.title, id.artist, id.durationSecs, t); if (p && p.status) apply(p); else await loadLyrics(true); return true; } catch (e) { player.error = String(e); return false; } } `; save(); load('src/mobile/HalfSheet.svelte'); rep(` import { lyr, nudge, searchCandidates, closeCandidates, chooseCandidate, forgetLyrics } from "./lyrics.svelte.js";`, ` import { lyr, nudge, searchCandidates, closeCandidates, chooseCandidate, forgetLyrics, pasteLyrics } from "./lyrics.svelte.js";`); rep(` {@render row("search", "Wrong lyrics? Search again", "LRCLIB, by this track's name", () => { void searchCandidates(); })}`, ` {@render row("search", "Wrong lyrics? Search again", "LRCLIB, by this track's name", () => { void searchCandidates(); })} {@render row("file-text", "Paste lyrics…", "plain text, or LRC with [mm:ss] timestamps", () => openHalf("prompt", { title: "Paste lyrics", placeholder: "[00:12.50] The first line…", ok: "Save", multiline: true, onsubmit: (v) => { void pasteLyrics(v); } }))}`); save(); // ── mocks ── load('src/lib/mockLibrary.js'); rep(` if (cmd === "lyrics_set_offset") return Promise.resolve();`, ` if (cmd === "lyrics_set_offset") return Promise.resolve(); if (cmd === "lyrics_save_text") { const lines = String(args?.text || "").split(/\\r?\\n/).map((l) => l.trim()).filter(Boolean); const timed = lines.map((l) => { const m = /^\\[(\\d+):(\\d\\d(?:\\.\\d+)?)\\]\\s*(.*)$/.exec(l); return m ? { time_ms: Math.round((+m[1] * 60 + +m[2]) * 1000), text: m[3] } : null; }); const synced = timed.every(Boolean); return Promise.resolve({ status: synced ? "synced" : "plain", source: "pasted", synced: synced ? timed : [], plain: synced ? null : lines.join("\\n"), offset_ms: 0, auto_offset_ms: 0, auto_sync_off: false }); }`); save(); // ── the walks ── load('tests/mobile/smoke.mjs'); rep(` { do: \`__click('.halfsheet .row', 'Live')\`, name: "lyrics-chosen", wait: 700 },`, ` { do: \`__click('.halfsheet .row', 'Live')\`, name: "lyrics-chosen", wait: 700 }, /* pasted words, timestamped: synced lines */ { do: \`__click('.lyr-tools', null)\`, wait: 600 }, { do: \`__click('.halfsheet .row', 'Paste lyrics')\`, wait: 600 }, { do: \`(function () { var i = document.querySelector('.halfsheet .prompt .field'); i.value = '[00:01.00] Pasted line one\\\\n[00:05.00] Pasted line two'; i.dispatchEvent(new Event('input', { bubbles: true })); })()\`, wait: 300 }, { do: \`__click('.halfsheet .prompt-acts .btn-fill', 'Save')\`, name: "lyrics-pasted", wait: 800 },`); rep(` "lyrics-chosen": (i) => !i.half.includes("up") && i.lyrLines >= 3,`, ` "lyrics-chosen": (i) => !i.half.includes("up") && i.lyrLines >= 3, "lyrics-pasted": (i) => !i.half.includes("up") && i.lyrLines === 2,`); rep(` { do: \`document.querySelector('.peer-file').dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))\`, name: "peer-file-sheet", wait: 700 },`, ` /* the hub's filters: FLAC only, then a free slot only */ { do: \`__click('.netfilters .chip', 'FLAC')\`, name: "net-filter-flac", wait: 500 }, { do: \`__click('.netfilters .chip', 'Free slot')\`, name: "net-filter-free", wait: 500 }, { do: \`__click('.netfilters .chip', 'Free slot')\`, wait: 300 }, { do: \`__click('.netfilters .chip', 'Any format')\`, wait: 400 }, { do: \`document.querySelector('.peer-file').dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))\`, name: "peer-file-sheet", wait: 700 },`); rep(` "peer-file-sheet": (i) => i.half.includes("up") && /Save this file/.test(i.halfText) && /Play from here/.test(i.halfText),`, ` "net-filter-flac": (i) => i.peers === 3, "net-filter-free": (i) => i.peers === 2, "peer-file-sheet": (i) => i.half.includes("up") && /Save this file/.test(i.halfText) && /Play from here/.test(i.halfText),`); save(); // ── the doc ── load('ANDROID.md'); rep(`The nav wears two badges the desktop's chrome has:`, `The network results carry the hub's filters (a free slot only; FLAC or MP3 320+), and the lyrics sheet has the desktop's paste door (plain text, or LRC with timestamps, kept for the track). The nav wears two badges the desktop's chrome has:`); save(); console.log('round eighteen wired');