From f792d7955d4c02cccda3a75e499b20907a01a337 Mon Sep 17 00:00:00 2001
From: KeelrLeek <156356974+KeelrLeek@users.noreply.github.com>
Date: Sun, 13 Sep 2026 22:28:38 -0500
Subject: [PATCH] =?UTF-8?q?android:=20a=20peer=20in=20full=20=E2=80=94=20t?=
=?UTF-8?q?heir=20card,=20their=20interests,=20and=20what=20they=20have=20?=
=?UTF-8?q?that=20you=20don't?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The peer sheet's new "About this user" row opens a screen of its own: the
card their client serves (their own words, their picture, the upload slot
and the queue), the likes and hates they declared, and the comparison this
phone makes of their share against the library. The comparison is the
point of opening a stranger — every record they have by an artist in your
library that you don't, each row opening the search that finds it, and a
way to forget the whole comparison again. Nothing is uploaded to work it
out. Their picture opens in the artwork viewer, which now takes a data:
key beside a library path and an https URL.
---
ANDROID.md | 14 +++
src/lib/mockLibrary.js | 53 ++++++++++
src/mobile/HalfSheet.svelte | 3 +
src/mobile/Mobile.svelte | 4 +
src/mobile/Viewer.svelte | 1 +
src/mobile/screens/Peer.svelte | 185 +++++++++++++++++++++++++++++++++
src/mobile/shell.css | 8 ++
tests/mobile/smoke.mjs | 24 +++++
8 files changed, 292 insertions(+)
create mode 100644 src/mobile/screens/Peer.svelte
diff --git a/ANDROID.md b/ANDROID.md
index 4571f92..127e8a6 100644
--- a/ANDROID.md
+++ b/ANDROID.md
@@ -231,6 +231,20 @@ played track under "On this phone"). Search corrects an obvious
misspelling from the names this phone knows (the library, the searches that worked) — the desktop's rule,
a word only when a real name is within two edits.
+Tapping **About this user** on the peer sheet opens the stranger in full
+(`screens/Peer.svelte`, `push("peer", { username })`): the card their own
+client serves (`slsk_user_info` — what they wrote about themselves, their
+picture, whether a slot is free, the queue, how much they have served),
+the likes and hates they declared to the server (`slsk_user_interests`),
+and the comparison this phone makes of their share against the library
+(`overlap.rs`: `overlap_compute` browses them on purpose, `overlap_list`
+reads what was compared before, `overlap_forget` drops it). Nothing is
+uploaded to work it out — their folder names against your albums, here.
+The comparison's rows are the reason to open a stranger: every record they
+have by an artist in your library that you don't, each one opening the
+catalog search that finds it. Their picture opens in the artwork viewer,
+which takes a `data:` key now as well as a library path and an https URL.
+
## Playlists
The desktop's two playlist pages (`src/views/Playlists.svelte`,
diff --git a/src/lib/mockLibrary.js b/src/lib/mockLibrary.js
index e75ed81..a779a0c 100644
--- a/src/lib/mockLibrary.js
+++ b/src/lib/mockLibrary.js
@@ -388,6 +388,22 @@ const PEERS = [
{ username: "mp3_marge", slot: true, speed: 500, queue: 0, ext: "mp3", bitrate: 320 },
{ username: "slowpoke", slot: false, speed: 60, queue: 12, ext: "mp3", bitrate: 192 },
];
+/* a peer's own card, and the comparison overlap.rs keeps per username */
+const MOCK_PIC = "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAEUlEQVR42mOQz+7HihiGlgQAV11GQceAU4sAAAAASUVORK5CYII=";
+const MOCK_ABOUT = "Ripping my own vinyl since 2004. FLAC only, folders named Artist - Year - Album. Ask before you queue a whole discography.";
+const MOCK_LIKES = ["french house", "vinyl rips", "detroit techno", "boards of canada"];
+const MOCK_HATES = ["transcodes", "128 kbps"];
+const OVERLAP_COMMON = [["Daft Punk", "Discovery"], ["Daft Punk", "Homework"], ["Caribou", "Swim"]];
+const OVERLAP_GAPS = [
+ ["Boards of Canada", "Geogaddi"],
+ ["Aphex Twin", "Drukqs"],
+ ["Daft Punk", "Alive 2007"],
+ ["Burial", "Untrue"],
+ ["Four Tet", "Rounds"],
+ ["Autechre", "Amber"],
+];
+const overlaps = new Map();
+const pairsOf = (list) => list.map(([artist, album]) => ({ artist, album }));
const TITLES = ["One More Time", "Aerodynamic", "Digital Love", "Harder, Better, Faster, Stronger", "Crescendolls", "Nightvision", "Superheroes", "High Life", "Something About Us", "Voyager", "Veridis Quo", "Short Circuit", "Face to Face", "Too Long"];
function peerResponse(token, peer, query) {
const artist = /daft/i.test(query) ? "Daft Punk" : (query.split(/\s+/)[0] || "Various");
@@ -601,6 +617,43 @@ function mockNet(cmd, args) {
{ name: "@@shared\\Music\\Scans", files: [{ name: "front.jpg", path: "@@shared\\Music\\Scans\\front.jpg", size: 1200000, ext: "jpg", bitrate: null, duration: null }] },
]), 600));
}
+ if (cmd === "slsk_user_info") {
+ const u = String(args?.username || "");
+ const peer = PEERS.find((x) => x.username === u);
+ return new Promise((r) => setTimeout(() => r({
+ username: u,
+ description: MOCK_ABOUT,
+ picture_png_base64: MOCK_PIC,
+ picture_note: null,
+ total_uploads: 18422,
+ queue_size: peer ? peer.queue : 0,
+ has_free_slot: peer ? peer.slot : true,
+ }), 450));
+ }
+ if (cmd === "slsk_user_interests") {
+ return new Promise((r) => setTimeout(() => r({ likes: MOCK_LIKES.slice(), hates: MOCK_HATES.slice() }), 400));
+ }
+ if (cmd === "overlap_list") return Promise.resolve([...overlaps.values()]);
+ if (cmd === "overlap_compute") {
+ /* the real one browses their whole share before it compares: the wait is the point */
+ const u = String(args?.username || "");
+ const o = {
+ username: u,
+ computed_at: Math.floor(Date.now() / 1000),
+ folders: 812,
+ audio_files: 9134,
+ their_albums: 74,
+ albums_in_common: pairsOf(OVERLAP_COMMON),
+ albums_in_common_count: OVERLAP_COMMON.length,
+ artists_in_common: ["Daft Punk", "Caribou"],
+ artists_in_common_count: 2,
+ gaps: pairsOf(OVERLAP_GAPS),
+ gaps_count: OVERLAP_GAPS.length,
+ score: OVERLAP_COMMON.length / 74,
+ };
+ return new Promise((r) => setTimeout(() => { overlaps.set(u.toLowerCase(), o); r(o); }, 1100));
+ }
+ if (cmd === "overlap_forget") { overlaps.delete(String(args?.username || "").toLowerCase()); return Promise.resolve(); }
if (cmd === "start_search") {
const t = ++token;
const q = String(args?.query || "");
diff --git a/src/mobile/HalfSheet.svelte b/src/mobile/HalfSheet.svelte
index 3c27465..722021e 100644
--- a/src/mobile/HalfSheet.svelte
+++ b/src/mobile/HalfSheet.svelte
@@ -511,6 +511,9 @@
{@render row("download", "Save this folder to library", "every file, as it is", act(() => saveGroup(g)))}
{/if}
+ {#if !(top().n === "peer" && top().c.username === u)}
+ {@render row("users", "About this user", "their card, and what they have that you don't", act(() => push("peer", { username: u })))}
+ {/if}
{#if !(top().n === "browse" && top().c.username === u)}
{@render row("folder-open", "Browse their files", "everything they share", act(() => push("browse", { username: u })))}
{/if}
diff --git a/src/mobile/Mobile.svelte b/src/mobile/Mobile.svelte
index 5ff1dfc..b8b50c0 100644
--- a/src/mobile/Mobile.svelte
+++ b/src/mobile/Mobile.svelte
@@ -25,6 +25,7 @@
import Playlist from "./screens/Playlist.svelte";
import Import from "./screens/Import.svelte";
import Browse from "./screens/Browse.svelte";
+ import Peer from "./screens/Peer.svelte";
import Community from "./screens/Community.svelte";
import Room from "./screens/Room.svelte";
import Thread from "./screens/Thread.svelte";
@@ -72,6 +73,7 @@
case "transfers": return { title: "Transfers", kicker: "Downloads" };
case "import": return { title: "Import", kicker: importer.draft?.source_name || "Playlist" };
case "browse": return { title: e.c.username, kicker: "Their files" };
+ case "peer": return { title: e.c.username, kicker: "On Soulseek" };
case "album": {
const a = albumById(e.c.id);
return { title: a?.title ?? "", kicker: a?.artist ?? "", amb: a?.art_path ?? null, flush: true };
@@ -234,6 +236,8 @@
{:else if entry.n === "browse"}
+ {:else if entry.n === "peer"}
+
{:else if entry.n === "community"}
{:else if entry.n === "room"}
diff --git a/src/mobile/Viewer.svelte b/src/mobile/Viewer.svelte
index b333d14..eec4ba1 100644
--- a/src/mobile/Viewer.svelte
+++ b/src/mobile/Viewer.svelte
@@ -16,6 +16,7 @@
scale = 1; tx = 0; ty = 0;
if (!k) return;
if (/^https?:/i.test(k)) { full = k; return; }
+ if (/^data:/i.test(k)) { full = k; return; } /* a peer's own picture: the bytes are already here */
if (/^stream:/.test(k)) return; /* a stream's tag art: the thumbnail is all there is */
let live = true;
api.albumArt(k, 2000).then((u) => { if (live && u) full = u; }, () => { /* the thumbnail stays */ });
diff --git a/src/mobile/screens/Peer.svelte b/src/mobile/screens/Peer.svelte
new file mode 100644
index 0000000..53ba9b5
--- /dev/null
+++ b/src/mobile/screens/Peer.svelte
@@ -0,0 +1,185 @@
+
+
+
+ {#if picture}
+
+ {:else}
+
+ {/if}
+
+
{username}
+
{uploadLine}
+
+
+
+
+
+
+
+
+
+{#if !online}
+
Sign in to Soulseek to ask their client for their card.
+{:else if infoErr}
+
Their client did not answer ({infoErr}) — offline, firewalled, or not sharing a card.
+{:else if !info}
+
Asking their client for the card — direct, peer to peer.
+{:else}
+
{info.description?.trim() || "They wrote nothing about themselves."}
+ {o.their_albums
+ ? `${o.albums_in_common_count} of their ${o.their_albums} albums are in your library · ${o.artists_in_common_count} artist${o.artists_in_common_count === 1 ? "" : "s"} in common · compared ${ago(o.computed_at)}`
+ : `No album folders among ${o.audio_files} audio files · compared ${ago(o.computed_at)}`}
+
+ {#if o.gaps.length}
+
They have {o.gaps_count} record{o.gaps_count === 1 ? "" : "s"} by artists in your library that you don't.{o.gaps.length > cap ? ` The first ${cap} are here.` : ""}