community: shared playlists, a signed revision and a pointer on the network, a reference over soulseek, an import through the normal door

This commit is contained in:
KeelrLeek committed 2026-09-02 15:02:06 -05:00
1 parent 984738135a
commit 13c44f731a
11 files changed
+1166 -3

No files matched your search

+63 -1
View File
@@ -72,6 +72,11 @@
friendshipFor,
friendCount,
pendingIn,
sharePreview,
shareImport,
shareDismiss,
sharedPublish,
sharedWithdraw,
} from "../lib/community.svelte.js";
onMount(() => {
@@ -423,6 +428,10 @@
const drawerLink = $derived(drawerUser ? linkFor(drawerUser) : null);
const drawerRemote = $derived(drawerLink ? community.remoteProfiles[drawerLink.persona_key] : null);
const drawerFriend = $derived(drawerUser ? friendshipFor(drawerUser) : null);
async function importShare(card) {
const id = await shareImport(card);
if (id) goToView("playlist", { playlistId: id, label: card.name });
}
const isMe = (u) => !!me && (u ?? "").trim().toLowerCase() === me.trim().toLowerCase();
$effect(() => {
const lk = drawerLink;
@@ -569,7 +578,36 @@
{/each}
</div>
{/if}
{#if !convos.length}
{#if community.shares.length}
<div class="freqs">
{#each community.shares as c (c.id)}
{@const pv = community.sharePreviews[c.id]}
<div class="card">
<div class="top">
<span class="mosaic-ph"><Icon name="playlist" size={22} /></span>
<div>
<div class="t">{c.name}</div>
<div class="s">{c.entry_count} {c.entry_count === 1 ? "track" : "tracks"} · playlist · shared by <button class="lnkname" onclick={() => (community.drawer = c.from_username)}>{display(c.from_username)}</button> {fmtWhen(c.received_at * 1000)}</div>
<div class="prov">
<span class="src ss"><Icon name="disc" size={10} />SoulStream</span>
<span>signed by {fpShort(c.owner_key)}</span>
{#if pv && !pv.pending && !pv.error && !pv.withdrawn}<span>·</span><span class="chip ok"><Icon name="check" size={9} />verified · revision {pv.revision_seq}</span>{/if}
{#if pv?.withdrawn}<span>·</span><span class="chip warn">withdrawn by the owner</span>{/if}
{#if pv?.error}<span>·</span><span class="chip err">{pv.error}</span>{/if}
</div>
</div>
</div>
<div class="bot">
<button class="ax-fill sm" onclick={() => importShare(c)} disabled={!nodeUp || !!community.busy["import:" + c.id] || pv?.withdrawn}><Icon name="download" size={12} /> {community.busy["import:" + c.id] ? "Importing…" : "Import"}</button>
<button class="ax-ghost sm" onclick={() => sharePreview(c)} disabled={!nodeUp || pv?.pending}>{pv?.pending ? "Fetching…" : "Verify"}</button>
<button class="lnk" onclick={() => shareDismiss(c.id)}>dismiss</button>
<span class="hint">tracks resolve against your library first; the rest become Soulseek streams</span>
</div>
</div>
{/each}
</div>
{/if}
{#if !convos.length && !community.shares.length && !pendingIn().length}
<div class="empty">
<Icon name="inbox" size={30} />
<div class="big">No conversations yet</div>
@@ -1044,6 +1082,26 @@
</div>
{#if key}
<div class="yc b">
<div class="panel">
<h3><Icon name="playlist" size={13} /> Your public playlists <span class="chip off">{community.published.filter((p) => !p.withdrawn).length}</span></h3>
{#if !community.published.length}
<p class="hint">Nothing published yet. Open a playlist and choose <b>Share on SoulStream</b>: a signed manifest goes to the network under your ID — titles, artists, albums, durations, your notes; never a file, never a path.</p>
{:else}
{#each community.published as p (p.playlist_id)}
<div class="pubrow">
<Icon name="playlist" size={14} />
<div class="ptx"><div class="nm">{p.name}</div><div class="hint">{p.entry_count} tracks · revision {p.revision_seq} · {p.withdrawn ? "withdrawn" : "published"} {fmtWhen(p.published_at * 1000)}</div></div>
{#if p.withdrawn}
<button class="ax-ghost sm" onclick={() => sharedPublish(p.playlist_id)} disabled={!nodeUp || !!community.busy["publish:" + p.playlist_id]}>Publish again</button>
{:else}
<button class="ax-ghost sm" onclick={() => sharedWithdraw(p.playlist_id)} disabled={!nodeUp || !!community.busy["publish:" + p.playlist_id]}>Withdraw</button>
{/if}
<button class="ax-ghost sm" onclick={() => goToView("playlist", { playlistId: p.playlist_id, label: p.name })}>Open</button>
</div>
{/each}
<p class="hint">Withdrawing stops your pointer; copies others already fetched stay with them.</p>
{/if}
</div>
<div class="panel">
<h3><Icon name="key" size={13} /> Your SoulStream ID</h3>
<p class="hint">
@@ -1704,6 +1762,10 @@
.rp .nm { font-size: 15px; font-weight: 800; display: flex; align-items: center; gap: 8px; }
.friendrow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
.mark.fr { color: var(--accent-hover); }
.pubrow { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-top: 1px solid var(--hair); }
.pubrow .ptx { flex: 1; min-width: 0; }
.pubrow .nm { font-size: 13px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mosaic-ph { width: 56px; height: 56px; border-radius: 8px; flex: none; display: grid; place-items: center; background: var(--accent-soft); color: var(--accent-hover); }
.dsec { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--hair); }
.dsec h4 { margin: 0 0 8px; font-size: 10.5px; font-weight: 900; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); display: flex; align-items: center; gap: 8px; }
.dsec h4 .src, .dsec h4 .chip { margin-left: auto; }