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
@@ -623,6 +623,12 @@ function mockNet(cmd, args) {
|
||||
]), 200));
|
||||
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_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 === "clear_play_history") return Promise.resolve();
|
||||
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 { slsk, refreshRooms, disconnect, openThread, addBuddy, removeBuddy, leaveRoom } from "./slsk.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 { note } from "./notice.svelte.js";
|
||||
import { fmtOf, albumFmt, fmtBytes, basename, dirname, longDur, mmss, titleFromName } from "./util.js";
|
||||
@@ -577,6 +577,7 @@
|
||||
<div class="sheet-hair"></div>
|
||||
{/if}
|
||||
{@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(); }))}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -105,3 +105,18 @@ export async function forgetLyrics() {
|
||||
try { await api.lyricsClear(id.title, id.artist, id.durationSecs); } catch { /* nothing kept */ }
|
||||
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(() => {});
|
||||
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) {
|
||||
const es = groupEntries(g, i);
|
||||
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}
|
||||
</div>
|
||||
{#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}
|
||||
<div class="sec-note">Asking the network as {slsk.username}…</div>
|
||||
{/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-head" oncontextmenu={(e) => { e.preventDefault(); openHalf("peer", { username: g.username, group: g }); }} role="presentation">
|
||||
<span class="peer-who">
|
||||
|
||||
Reference in new issue
Block a user