android: the hub's filters on the network results, pasted lyrics

The raw results take the Soulseek hub's filters — a free slot only, FLAC
or MP3 320+ — as chips above the folders. The lyrics sheet has the
desktop's paste door: plain text, or LRC with timestamps, kept for the
track.
This commit is contained in:
KeelrLeek committed 2026-09-13 22:03:57 -05:00
1 parent 59be9f6c3e
commit 3c8989dfa6
6 files changed
+58 -3

No files matched your search

+3 -1
View File
@@ -213,7 +213,9 @@ Save menu: Save to library (at once when the cache is byte-complete, else
through the download pipeline) and Save this record for the streamed through the download pipeline) and Save this record for the streamed
record the queue holds. A moment after boot the phone warms the library's thumbnails the record the queue holds. A moment after boot the phone warms the library's thumbnails the
desktop's way (`library_art_prewarm` at the cards' edge, one decode at a desktop's way (`library_art_prewarm` at the cards' edge, one decode at a
time behind the tiles' gate). The nav wears two badges the desktop's chrome has: transfers in flight on time behind the tiles' gate). 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: transfers in flight on
You (the title bar's download ring) and unread chats on Community. You (the title bar's download ring) and unread chats on Community.
The Library's count line carries the collection's hours and the sort The Library's count line carries the collection's hours and the sort
offers By format (lossless by depth and rate first, then by bitrate); the offers By format (lossless by depth and rate first, then by bitrate); the
+6
View File
@@ -623,6 +623,12 @@ function mockNet(cmd, args) {
]), 200)); ]), 200));
if (cmd === "lyrics_select") return Promise.resolve({ ...mockLyrics(args?.title), source: "lrclib #" + args?.candidateId }); if (cmd === "lyrics_select") return Promise.resolve({ ...mockLyrics(args?.title), source: "lrclib #" + args?.candidateId });
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 });
}
if (cmd === "lyrics_clear") return Promise.resolve(); if (cmd === "lyrics_clear") return Promise.resolve();
if (cmd === "clear_play_history") return Promise.resolve(); if (cmd === "clear_play_history") return Promise.resolve();
if (cmd === "refresh_album_art") return new Promise((r) => setTimeout(() => r("C:\\mock\\cover.jpg"), 200)); if (cmd === "refresh_album_art") return new Promise((r) => setTimeout(() => r("C:\\mock\\cover.jpg"), 200));
+2 -1
View File
@@ -16,7 +16,7 @@
import { DOORS, start as startImport } from "./importer.svelte.js"; import { DOORS, start as startImport } from "./importer.svelte.js";
import { slsk, refreshRooms, disconnect, openThread, addBuddy, removeBuddy, leaveRoom } from "./slsk.svelte.js"; import { slsk, refreshRooms, disconnect, openThread, addBuddy, removeBuddy, leaveRoom } from "./slsk.svelte.js";
import { lib, albumById, albumsBy, tracksOf, loadLibrary, refreshPlaylists, refreshRecents } from "./lib.svelte.js"; import { lib, albumById, albumsBy, tracksOf, loadLibrary, refreshPlaylists, refreshRecents } from "./lib.svelte.js";
import { lyr, nudge, searchCandidates, closeCandidates, chooseCandidate, forgetLyrics } from "./lyrics.svelte.js"; import { lyr, nudge, searchCandidates, closeCandidates, chooseCandidate, forgetLyrics, pasteLyrics } from "./lyrics.svelte.js";
import { art, wantArt } from "./art.svelte.js"; import { art, wantArt } from "./art.svelte.js";
import { note } from "./notice.svelte.js"; import { note } from "./notice.svelte.js";
import { fmtOf, albumFmt, fmtBytes, basename, dirname, longDur, mmss, titleFromName } from "./util.js"; import { fmtOf, albumFmt, fmtBytes, basename, dirname, longDur, mmss, titleFromName } from "./util.js";
@@ -577,6 +577,7 @@
<div class="sheet-hair"></div> <div class="sheet-hair"></div>
{/if} {/if}
{@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); } }))}
{@render row("refresh", "Forget and look again", "", act(() => { void forgetLyrics(); }))} {@render row("refresh", "Forget and look again", "", act(() => { void forgetLyrics(); }))}
</div> </div>
{/if} {/if}
+15
View File
@@ -105,3 +105,18 @@ export async function forgetLyrics() {
try { await api.lyricsClear(id.title, id.artist, id.durationSecs); } catch { /* nothing kept */ } try { await api.lyricsClear(id.title, id.artist, id.durationSecs); } catch { /* nothing kept */ }
await loadLyrics(true); await loadLyrics(true);
} }
/** 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;
}
}
+19 -1
View File
@@ -263,6 +263,15 @@
netSearch(query).catch(() => {}); netSearch(query).catch(() => {});
inputEl?.blur(); inputEl?.blur();
} }
/* 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) { function playPeer(g, i) {
const es = groupEntries(g, i); const es = groupEntries(g, i);
if (es.length) playEntries(es, 0, g.folderName).catch((e) => { player.error = String(e); }); if (es.length) playEntries(es, 0, g.folderName).catch((e) => { player.error = String(e); });
@@ -421,10 +430,19 @@
{#if !net.searching}<button class="all net-ask" onclick={askNetwork}>Again<Icon name="refresh" size={14} /></button>{/if} {#if !net.searching}<button class="all net-ask" onclick={askNetwork}>Again<Icon name="refresh" size={14} /></button>{/if}
</div> </div>
{#if net.error}<div class="sec-note">{net.error}</div>{/if} {#if net.error}<div class="sec-note">{net.error}</div>{/if}
{#if net.groups.length}
<div class="chips netfilters" style="padding-bottom: 10px">
<button class="chip" class:on={netFree} class:quiet={!netFree} onclick={() => (netFree = !netFree)}>Free slot</button>
{#each [["any", "Any format"], ["flac", "FLAC"], ["mp3320", "MP3 320+"]] as [v, l] (v)}
<button class="chip" class:on={netFmt === v} class:quiet={netFmt !== v} onclick={() => (netFmt = v)}>{l}</button>
{/each}
</div>
{#if shownGroups.length !== net.groups.length}<div class="sec-note">{shownGroups.length} of {net.groups.length} folders</div>{/if}
{/if}
{#if net.searching && !net.groups.length} {#if net.searching && !net.groups.length}
<div class="sec-note">Asking the network as {slsk.username}…</div> <div class="sec-note">Asking the network as {slsk.username}…</div>
{/if} {/if}
{#each net.groups.slice(0, 40) as g (g.key)} {#each shownGroups.slice(0, 40) as g (g.key)}
<div class="peer"> <div class="peer">
<div class="peer-head" oncontextmenu={(e) => { e.preventDefault(); openHalf("peer", { username: g.username, group: g }); }} role="presentation"> <div class="peer-head" oncontextmenu={(e) => { e.preventDefault(); openHalf("peer", { username: g.username, group: g }); }} role="presentation">
<span class="peer-who"> <span class="peer-who">
+13
View File
@@ -195,6 +195,11 @@ const MOCK_STEPS = [
{ do: `__click('.halfsheet .row', 'Lyrics later')`, name: "lyrics-nudged", wait: 500 }, { do: `__click('.halfsheet .row', 'Lyrics later')`, name: "lyrics-nudged", wait: 500 },
{ do: `__click('.halfsheet .row', 'Search again')`, name: "lyrics-candidates", wait: 900 }, { do: `__click('.halfsheet .row', 'Search again')`, name: "lyrics-candidates", wait: 900 },
{ 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 },
{ do: `document.querySelectorAll('.fbtn')[0].click()`, wait: 300 }, { do: `document.querySelectorAll('.fbtn')[0].click()`, wait: 300 },
{ do: `history.back()`, name: "after-back", wait: 900 }, { do: `history.back()`, name: "after-back", wait: 900 },
{ do: `document.querySelector('.rec-artist').click()`, name: "artist", wait: 900 }, { do: `document.querySelector('.rec-artist').click()`, name: "artist", wait: 900 },
@@ -247,6 +252,11 @@ const MOCK_STEPS = [
{ do: `__settle()`, wait: 800 }, { do: `__settle()`, wait: 800 },
{ do: `document.querySelectorAll('.nav button')[1].click()`, wait: 700 }, { do: `document.querySelectorAll('.nav button')[1].click()`, wait: 700 },
{ do: `__click('.net-ask', null)`, name: "net-peers", wait: 1800 }, { do: `__click('.net-ask', null)`, name: "net-peers", wait: 1800 },
/* 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 }, { do: `document.querySelector('.peer-file').dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))`, name: "peer-file-sheet", wait: 700 },
{ do: `document.querySelector('.scrim').click()`, wait: 500 }, { do: `document.querySelector('.scrim').click()`, wait: 500 },
{ do: `document.querySelector('.peer-head .peer-menu').click()`, name: "peer-sheet", wait: 700 }, { do: `document.querySelector('.peer-head .peer-menu').click()`, name: "peer-sheet", wait: 700 },
@@ -423,6 +433,7 @@ const MOCK_EXPECT = {
"lyrics-nudged": (i) => /\+0\.5 s/.test(i.sheetTitle), "lyrics-nudged": (i) => /\+0\.5 s/.test(i.sheetTitle),
"lyrics-candidates": (i) => /Choose the right lyrics/.test(i.halfText) && /Live/.test(i.halfText), "lyrics-candidates": (i) => /Choose the right lyrics/.test(i.halfText) && /Live/.test(i.halfText),
"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,
viewer: (i) => i.viewer, viewer: (i) => i.viewer,
"viewer-closed": (i) => !i.viewer && i.sheet.includes("up"), "viewer-closed": (i) => !i.viewer && i.sheet.includes("up"),
"album-playing": (i) => i.mini, "album-playing": (i) => i.mini,
@@ -457,6 +468,8 @@ const MOCK_EXPECT = {
"np-more": (i) => i.half.includes("up") && /Save to library/.test(i.halfText), "np-more": (i) => i.half.includes("up") && /Save to library/.test(i.halfText),
"np-saved": (i) => !i.half.includes("up") && /Saved to the library/.test(i.notice), "np-saved": (i) => !i.half.includes("up") && /Saved to the library/.test(i.notice),
"net-peers": (i) => i.title.startsWith("Search") && i.peers >= 3, "net-peers": (i) => i.title.startsWith("Search") && i.peers >= 3,
"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), "peer-file-sheet": (i) => i.half.includes("up") && /Save this file/.test(i.halfText) && /Play from here/.test(i.halfText),
"peer-sheet": (i) => i.half.includes("up") && /Browse their files/.test(i.halfText) && /Play this folder/.test(i.halfText), "peer-sheet": (i) => i.half.includes("up") && /Browse their files/.test(i.halfText) && /Play this folder/.test(i.halfText),
browse: (i) => i.screen.includes("tr-push") && i.brwDirs >= 2 && /folders/.test(i.brwHead), browse: (i) => i.screen.includes("tr-push") && i.brwDirs >= 2 && /folders/.test(i.brwHead),