community: one area for the people on both networks, and the hub grows search tabs, a folder column and a queue column

This commit is contained in:
KeelrLeek committed 2026-09-02 13:43:16 -05:00
1 parent 6dce4e2ac7
commit 8c25f93afe
14 files changed
+2368 -68

No files matched your search

+1 -1
View File
@@ -31,7 +31,7 @@
// The Soulseek hub carries Qt's connection status in the bar's right —
// "online as X · sharing N files" — the way the Qt titlebar did.
const HUB_VIEWS = new Set(["soulseek", "search", "downloads"]);
const HUB_VIEWS = new Set(["soulseek", "search", "downloads", "community"]);
const showConn = $derived(HUB_VIEWS.has(ui.view));
import { canGoBack, canGoForward, trail } from "../lib/navigation.svelte.js";
import { markAppScroll, appScrollIdleFor } from "../lib/scrollbar.js";
+16
View File
@@ -38,6 +38,22 @@
trash: '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>',
expand: '<polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" x2="14" y1="3" y2="10"/><line x1="3" x2="10" y1="21" y2="14"/>',
shrink: '<polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" x2="21" y1="10" y2="3"/><line x1="3" x2="10" y1="21" y2="14"/>',
// The Community area's glyphs (2026-09-02).
hash: '<line x1="4" x2="20" y1="9" y2="9"/><line x1="4" x2="20" y1="15" y2="15"/><line x1="10" x2="8" y1="3" y2="21"/><line x1="16" x2="14" y1="3" y2="21"/>',
inbox: '<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>',
compass: '<circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>',
sparkle: '<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>',
message: '<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/>',
send: '<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/>',
copy: '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
key: '<path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"/><path d="m21 2-9.6 9.6"/><circle cx="7.5" cy="15.5" r="5.5"/>',
shield: '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/>',
ban: '<circle cx="12" cy="12" r="10"/><path d="m4.9 4.9 14.2 14.2"/>',
eye: '<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/>',
star: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>',
bell: '<path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/>',
more: '<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>',
devices: '<rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/>',
"chevron-left": '<path d="m15 18-6-6 6-6"/>',
"chevron-right": '<path d="m9 18 6-6-6-6"/>',
"chevron-down": '<path d="m6 9 6 6 6-6"/>',
+133
View File
@@ -0,0 +1,133 @@
<script module>
function h32(s) {
let h = 0x811c9dc5;
for (const c of String(s)) {
h ^= c.charCodeAt(0);
h = Math.imul(h, 0x01000193) >>> 0;
}
return h;
}
function rng(seed) {
let x = seed || 7;
return () => {
x ^= x << 13;
x >>>= 0;
x ^= x >>> 17;
x ^= x << 5;
x >>>= 0;
return x / 4294967296;
};
}
function arc(cx, cy, r, a0, a1) {
const p = (a) => [cx + r * Math.cos((a * Math.PI) / 180), cy + r * Math.sin((a * Math.PI) / 180)];
const [x0, y0] = p(a0);
const [x1, y1] = p(a1);
return `M${x0.toFixed(2)} ${y0.toFixed(2)}A${r} ${r} 0 ${a1 - a0 > 180 ? 1 : 0} 1 ${x1.toFixed(2)} ${y1.toFixed(2)}`;
}
/** The hue this seed paints with — the persona ambience borrows it. */
export function hueOf(seed) {
return h32(seed) % 360;
}
</script>
<script>
// The SoulStream ID made visible: a record whose grooves are cut from
// the key. Deterministic — the same seed draws the same face on every
// machine, so a person is recognisable without ever uploading anything.
// Approved on the Community mockup (research/community-mockups,
// 2026-09-02, "Grooves"). A verified avatar photo replaces the grooves;
// the presence dot is Soulseek's only — Freenet never says "online".
let {
seed = "",
size = 40,
/** A verified, sanitized avatar as a data URL — replaces the grooves. */
photo = null,
/** "online" | "away" | "off" | null (no dot at all). */
presence = null,
/** Accent ring — the identity strip's own face. */
ring = false,
/** Rounded square instead of a disc (Spaces, rooms). */
square = false,
} = $props();
const svg = $derived.by(() => {
const r = rng(h32(seed));
const hue = h32(seed) % 360;
const hue2 = (hue + 150 + Math.floor(r() * 60)) % 360;
const c1 = `hsl(${hue} 64% 60%)`;
const c2 = `hsl(${hue2} 58% 64%)`;
const bg = `hsl(${hue} 34% 13%)`;
const bg2 = `hsl(${hue} 30% 20%)`;
let inner = "";
[11, 16, 21, 26].forEach((rad, i) => {
const segs = 6 + i * 2;
const gap = 8;
for (let s = 0; s < segs; s++) {
if (r() < 0.38) continue;
const a0 = (s * 360) / segs + gap / 2;
const a1 = ((s + 1) * 360) / segs - gap / 2;
inner += `<path d="${arc(32, 32, rad, a0, a1)}" stroke="${r() > 0.72 ? c2 : c1}" stroke-width="3.6" fill="none" stroke-linecap="round"/>`;
}
});
return `<svg viewBox="0 0 64 64" aria-hidden="true"><rect width="64" height="64" fill="${bg}"/><circle cx="32" cy="32" r="30.5" fill="${bg2}"/>${inner}<circle cx="32" cy="32" r="5.2" fill="${bg}"/><circle cx="32" cy="32" r="1.6" fill="${c1}"/></svg>`;
});
</script>
<span class="av" class:ring class:square style="width:{size}px;height:{size}px">
{#if photo}
<img src={photo} alt="" draggable="false" />
{:else}
{@html svg}
{/if}
{#if presence}
<span class="pd {presence}" style="--d:{Math.max(8, Math.round(size * 0.27))}px"></span>
{/if}
</span>
<style>
.av {
position: relative;
flex: none;
display: inline-block;
border-radius: 50%;
vertical-align: middle;
}
.av.square {
border-radius: 22%;
}
.av :global(svg),
.av img {
width: 100%;
height: 100%;
border-radius: inherit;
object-fit: cover;
display: block;
}
.av img {
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
.av.ring {
box-shadow:
0 0 0 2px var(--bg-base),
0 0 0 3.5px var(--accent);
}
/* The dot sits on the pane, so its cut-out ring is the pane's own base
colour; under glass that is a solid dark ring on the frost — read as a
socket, which is fine. */
.pd {
position: absolute;
right: -1px;
bottom: -1px;
width: var(--d);
height: var(--d);
border-radius: 50%;
background: #3ddc84;
box-shadow: 0 0 0 2.5px var(--bg-base);
}
.pd.away {
background: #e8a33d;
}
.pd.off {
background: rgba(255, 255, 255, 0.28);
}
</style>
+1
View File
@@ -191,6 +191,7 @@
const items = [
{ id: "home", label: "Home", icon: "home" },
{ id: "soulseek", label: "Soulseek", icon: "globe" },
{ id: "community", label: "Community", icon: "users" },
{ id: "library", label: "Your Library", icon: "library" },
{ id: "settings", label: "Settings", icon: "settings" },
{ id: "playlists", label: "Playlists", icon: "playlist" },