forked from Keelrleek/SoulStream
who has this: a gap card says who holds it, the sheet and the open album name them, a Space grows a Holdings tab, and Get it is the hub's own grab or a wait for their presence
This commit is contained in:
1 parent
a98c750908
commit
b8e6fb7396
11 files changed
+454
-6
No files matched your search
@@ -13,6 +13,7 @@
|
||||
import { onMount, untrack } from "svelte";
|
||||
import Icon from "../components/Icon.svelte";
|
||||
import Identicon, { hueOf } from "../components/Identicon.svelte";
|
||||
import WhoHas from "../components/WhoHas.svelte";
|
||||
import { rankPeople, reasonParts } from "../lib/discover.js";
|
||||
import Soulseek from "./Soulseek.svelte";
|
||||
import * as api from "../lib/api.js";
|
||||
@@ -96,6 +97,9 @@
|
||||
pinToSpace,
|
||||
leaveSpace,
|
||||
setShareListens,
|
||||
refreshHoldings,
|
||||
holdingsFor,
|
||||
setShareHoldings,
|
||||
openBoard,
|
||||
proposeToBoard,
|
||||
decideProposal,
|
||||
@@ -548,6 +552,18 @@
|
||||
let spaceCreating = $state(false);
|
||||
const sv = $derived(community.space);
|
||||
const spaceOwner = $derived(sv?.my_role === "owner");
|
||||
// Holdings: the room's lists are read when a Space opens (stale members
|
||||
// in the background; the event brings the result back), and the ask box
|
||||
// answers from the cache.
|
||||
const hv = $derived(sv ? holdingsFor(sv.owner, sv.space_id) : null);
|
||||
let askArtist = $state("");
|
||||
let askAlbum = $state("");
|
||||
let asked = $state(null);
|
||||
$effect(() => {
|
||||
const id = sv?.id;
|
||||
if (!id) return;
|
||||
untrack(() => void refreshHoldings(sv.owner, sv.space_id));
|
||||
});
|
||||
let spaceEl = $state(null);
|
||||
$effect(() => {
|
||||
const n = sv?.messages.length;
|
||||
@@ -1362,6 +1378,7 @@
|
||||
<div class="sptabs">
|
||||
<button class="subtab" class:on={spaceTab === "chat"} onclick={() => (spaceTab = "chat")}>Chat<b>{sv.messages.length}</b></button>
|
||||
<button class="subtab" class:on={spaceTab === "listening"} onclick={() => (spaceTab = "listening")}>Listening{#if sv.listens.length}<b>{sv.listens.length}</b>{/if}</button>
|
||||
<button class="subtab" class:on={spaceTab === "holdings"} onclick={() => (spaceTab = "holdings")}>Holdings{#if hv?.sharing}<b>{hv.sharing}</b>{/if}</button>
|
||||
<button class="subtab" class:on={spaceTab === "members"} onclick={() => (spaceTab = "members")}>Members<b>{sv.members.length}</b></button>
|
||||
<button class="subtab" class:on={spaceTab === "about"} onclick={() => (spaceTab = "about")}>About</button>
|
||||
<button class="subtab" class:on={spaceTab === "proposals"} onclick={() => (spaceTab = "proposals")}>Proposals{#if bv && bv.uuid === sv.pinned_uuid && bv.proposals.length}<b>{bv.proposals.length}</b>{/if}</button>
|
||||
@@ -1446,6 +1463,50 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if spaceTab === "holdings"}
|
||||
<div class="panel sharebar" style="margin-bottom:12px">
|
||||
<button class="toggle" class:on={sv.share_holdings} aria-pressed={sv.share_holdings} title="Share what I hold in this Space"
|
||||
onclick={() => setShareHoldings(sv.owner, sv.space_id, !sv.share_holdings)} disabled={!nodeUp || !sv.can_post || !!community.busy["holdings:share"]}><i></i></button>
|
||||
<div class="grow">
|
||||
<div class="ttl">Share what I hold in this Space</div>
|
||||
<p class="hint">{#if sv.share_holdings && hv?.mine && !hv.mine.withdrawn}<b>{fmtCount(hv.mine.count)} albums</b> · published{#if sv.private} sealed to this room's epoch key{/if} · updated when your library changes · <b>synced as of {fmtClock(hv.mine.published_at * 1000)}</b>{:else}Album names as keys, one quality word each, and your Soulseek name so members can get it from you — never files. Per Space, off until you say so.{/if}</p>
|
||||
</div>
|
||||
{#if community.busy["holdings:share"]}<span class="chip off">working…</span>{:else if sv.share_holdings}<span class="chip acc">sharing</span>{:else}<span class="chip off">not sharing</span>{/if}
|
||||
</div>
|
||||
<div class="disc"><Icon name="alert" size={14} /><span><b>What the room learns:</b> members can tell whether you hold any album they can name, and roughly how many you hold. They never see files, paths, or formats beyond a lossless/lossy word. Turning this off withdraws the list; copies members already read stay with them.</span></div>
|
||||
<div class="panel" style="margin-top:12px">
|
||||
<h3><Icon name="search" size={13} /> Ask this room{#if hv}<span class="chip up">{hv.sharing} of {sv.members.length} sharing</span>{/if}</h3>
|
||||
<form class="inl askinl" style="margin-top:8px" onsubmit={(e) => { e.preventDefault(); if (askArtist.trim() && askAlbum.trim()) asked = { artist: askArtist.trim(), album: askAlbum.trim() }; }}>
|
||||
<input placeholder="Artist" bind:value={askArtist} />
|
||||
<input placeholder="Album" bind:value={askAlbum} />
|
||||
<button class="ax-ghost sm" type="submit" disabled={!askArtist.trim() || !askAlbum.trim()}>Ask</button>
|
||||
</form>
|
||||
{#if asked}{#key asked.artist + "\u0000" + asked.album}<div style="margin-top:8px"><WhoHas artist={asked.artist} album={asked.album} /></div>{/key}{/if}
|
||||
</div>
|
||||
<div class="panel" style="margin-top:12px">
|
||||
<h3><Icon name="users" size={13} /> Who shares here <button class="ax-ghost sm" style="margin-left:auto" onclick={() => refreshHoldings(sv.owner, sv.space_id, true)} disabled={!nodeUp}><Icon name="refresh" size={12} /> Refresh</button></h3>
|
||||
{#if hv}
|
||||
<div class="hlist">
|
||||
<div class="hrow">
|
||||
<Identicon seed={key ?? "you"} size={30} />
|
||||
<span class="tx"><span class="l1">you</span>
|
||||
<span class="l2">{#if sv.share_holdings && hv.mine && !hv.mine.withdrawn}<b>{fmtCount(hv.mine.count)}</b> albums · list {hv.mine.sequence} · published {fmtClock(hv.mine.published_at * 1000)}{:else}not sharing{/if}</span></span>
|
||||
<span class="chip" class:acc={sv.share_holdings && hv.mine && !hv.mine.withdrawn} class:off={!(sv.share_holdings && hv.mine && !hv.mine.withdrawn)}>{sv.share_holdings && hv.mine && !hv.mine.withdrawn ? "shared" : "none"}</span>
|
||||
</div>
|
||||
{#each hv.members.filter((m) => !m.is_me) as m (m.key)}
|
||||
<div class="hrow">
|
||||
<Identicon seed={m.username ?? m.key} size={30} />
|
||||
<span class="tx"><span class="l1">{m.is_me ? "you" : m.username ? display(m.username) : fpShort(m.key)}</span>
|
||||
<span class="l2">{#if m.state === "shared"}<b>{fmtCount(m.count)}</b> albums · list {m.sequence}{#if m.read_at} · read {fmtClock(m.read_at * 1000)}{/if}{:else if m.state === "none"}not sharing{:else if m.state === "withdrawn"}withdrew their list{:else if m.state === "sealed"}sealed to an epoch you have no key for{:else}not read yet{/if}</span></span>
|
||||
<span class="chip" class:acc={m.state === "shared"} class:off={m.state !== "shared"}>{m.state}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="hint" style="margin-top:8px">Reading the room's lists…</p>
|
||||
{/if}
|
||||
<p class="hint" style="margin-top:8px">A member's list is read when the Space opens and again after ten minutes; a member who never published costs a slow search, so they are asked again only hourly.</p>
|
||||
</div>
|
||||
{:else if spaceTab === "members"}
|
||||
<div class="panel">
|
||||
<div class="members">
|
||||
@@ -2390,6 +2451,18 @@
|
||||
.mem .nm { font-size: 11.5px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 96px; }
|
||||
.mem .role { font-size: 9.5px; font-weight: 900; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
|
||||
.mem .role.own { color: #d9a6ff; }
|
||||
/* holdings: the disclosure and the who-shares list */
|
||||
.disc { display: flex; gap: 8px; align-items: flex-start; padding: 9px 11px; border-radius: 9px; background: color-mix(in srgb, var(--warn) 7%, transparent); border: 1px solid color-mix(in srgb, var(--warn) 25%, transparent); font-size: 11.5px; color: var(--text-secondary); line-height: 1.5; }
|
||||
.disc b { color: #f0c070; }
|
||||
.askinl input { height: 30px; padding: 0 10px; border-radius: 8px; background: var(--surf-hi); border: none; color: var(--text-primary); font-size: 12px; outline: none; }
|
||||
.askinl input::placeholder { color: var(--text-muted); }
|
||||
.hlist { display: flex; flex-direction: column; gap: 2px; margin-top: 8px; }
|
||||
.hrow { display: flex; align-items: center; gap: 10px; padding: 5px 8px; border-radius: 8px; min-height: 40px; }
|
||||
.hrow:hover { background: var(--surf-hi); }
|
||||
.hrow .tx { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
||||
.hrow .l1 { font-size: 13px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.hrow .l2 { font-size: 11.5px; color: var(--text-muted); }
|
||||
.hrow .l2 b { color: var(--text-secondary); }
|
||||
/* ── you ── */
|
||||
/* The You section sized by the PANE (container query), never the window:
|
||||
three columns from ~1240px of pane, two down to ~820, then one. */
|
||||
|
||||
Reference in new issue
Block a user