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:
1 parent
6dce4e2ac7
commit
8c25f93afe
14 files changed
+2368
-68
No files matched your search
@@ -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";
|
||||
|
||||
@@ -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"/>',
|
||||
|
||||
@@ -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>
|
||||
@@ -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" },
|
||||
|
||||
Reference in new issue
Block a user