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:
1 parent
59be9f6c3e
commit
3c8989dfa6
6 files changed
+58
-3
No files matched your search
+3
-1
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in new issue
Block a user