forked from Keelrleek/SoulStream
community: the library overlap shelf, their share folders against your albums on this device after a browse
This commit is contained in:
1 parent
d7bc92fda7
commit
0846b39d45
6 files changed
+451
-7
No files matched your search
@@ -185,6 +185,9 @@ export const spaceLeave = (owner, space) => invoke("space_leave", { owner, space
|
||||
export const spaceBoard = (owner, space) => invoke("space_board", { owner, space });
|
||||
export const spacePropose = (owner, space, base, ops, note) => invoke("space_propose", { owner, space, base, ops, note });
|
||||
export const spaceDecide = (owner, space, proposal, accept, reason) => invoke("space_decide", { owner, space, proposal, accept, reason });
|
||||
export const overlapList = () => invoke("overlap_list");
|
||||
export const overlapCompute = (username) => invoke("overlap_compute", { username });
|
||||
export const overlapForget = (username) => invoke("overlap_forget", { username });
|
||||
export const buddyList = () => invoke("buddy_list");
|
||||
export const buddyAdd = (username) => invoke("buddy_add", { username });
|
||||
export const buddyRemove = (username) => invoke("buddy_remove", { username });
|
||||
|
||||
@@ -83,6 +83,8 @@ export const community = $state({
|
||||
/** space_board: the proposal board beside the open Space's pinned playlist, or null. */
|
||||
board: null,
|
||||
boardError: "",
|
||||
/** overlap_list: library comparisons computed on this device, best first. */
|
||||
overlap: [],
|
||||
/** persona key → ProfileView | {none} | {error} | {pending}: other
|
||||
* people's signed profiles, read once per session when surfaced. */
|
||||
remoteProfiles: {},
|
||||
@@ -335,11 +337,12 @@ export async function initCommunity() {
|
||||
void refreshShares();
|
||||
void refreshPublished();
|
||||
});
|
||||
api.onEvent("community:overlap", () => void refreshOverlap());
|
||||
api.onEvent("soulseek:user-state", (p) => {
|
||||
if (p.status === 1 || p.status === 2) noteSeen(p.username);
|
||||
});
|
||||
}
|
||||
await Promise.all([refreshFreenet(), loadContacts(), refreshLinks(), refreshFriends(), refreshShares(), refreshPublished(), refreshInbox(), refreshSpaces()]);
|
||||
await Promise.all([refreshFreenet(), loadContacts(), refreshLinks(), refreshFriends(), refreshShares(), refreshPublished(), refreshInbox(), refreshSpaces(), refreshOverlap()]);
|
||||
await refreshPersona();
|
||||
}
|
||||
|
||||
@@ -581,6 +584,30 @@ export const leaveSpace = (owner, space) =>
|
||||
await refreshSpaces();
|
||||
});
|
||||
|
||||
// ---- library overlap ------------------------------------------------------------
|
||||
export async function refreshOverlap() {
|
||||
try {
|
||||
community.overlap = await api.overlapList();
|
||||
} catch {
|
||||
// stays as it was
|
||||
}
|
||||
}
|
||||
export const overlapFor = (username) => {
|
||||
const k = (username ?? "").trim().toLowerCase();
|
||||
return community.overlap.find((o) => o.username.toLowerCase() === k) ?? null;
|
||||
};
|
||||
/** Browse them on purpose and compare — nothing is uploaded to do it. */
|
||||
export const computeOverlap = (username) =>
|
||||
run("overlap:" + username, async () => {
|
||||
await api.overlapCompute(username);
|
||||
await refreshOverlap();
|
||||
});
|
||||
export const forgetOverlap = (username) =>
|
||||
run("overlap:" + username, async () => {
|
||||
await api.overlapForget(username);
|
||||
await refreshOverlap();
|
||||
});
|
||||
|
||||
// ---- the proposal board ---------------------------------------------------------
|
||||
/** Read the board beside the Space's pinned playlist; the playlist's owner also
|
||||
* brings its roster in line with the members here. */
|
||||
@@ -621,4 +648,4 @@ export const decideProposal = (owner, space, proposal, accept, reason) =>
|
||||
// Dev handle beside `__soulstream` (state.svelte.js): probes reach the store
|
||||
// through this, never through a bare import (which would be a second copy).
|
||||
if (import.meta.env.DEV) globalThis.__community = community;
|
||||
if (import.meta.env.DEV) globalThis.__communityActions = { friendRequest, friendRequestByKey, friendAccept, friendDecline, friendRemove, friendForget, checkLink, refreshFriends, sharedPublish, sharedWithdraw, shareSend, sharePreview, shareImport, refreshShares, refreshPublished, publishInbox, checkInbox, refreshInbox, refreshSpaces, openSpace, createSpace, postToSpace, inviteToSpace, removeFromSpace, pinToSpace, leaveSpace, openBoard, proposeToBoard, decideProposal };
|
||||
if (import.meta.env.DEV) globalThis.__communityActions = { friendRequest, friendRequestByKey, friendAccept, friendDecline, friendRemove, friendForget, checkLink, refreshFriends, sharedPublish, sharedWithdraw, shareSend, sharePreview, shareImport, refreshShares, refreshPublished, publishInbox, checkInbox, refreshInbox, refreshSpaces, openSpace, createSpace, postToSpace, inviteToSpace, removeFromSpace, pinToSpace, leaveSpace, openBoard, proposeToBoard, decideProposal, refreshOverlap, computeOverlap, forgetOverlap };
|
||||
@@ -91,6 +91,9 @@
|
||||
openBoard,
|
||||
proposeToBoard,
|
||||
decideProposal,
|
||||
overlapFor,
|
||||
computeOverlap,
|
||||
forgetOverlap,
|
||||
} from "../lib/community.svelte.js";
|
||||
|
||||
onMount(() => {
|
||||
@@ -583,6 +586,7 @@
|
||||
const decideOn = (p, accept) =>
|
||||
decideProposal(sv.owner, sv.space_id, p.id, accept, boardReason[p.id] || (!accept && p.stale ? "stale base — re-submit against the current revision" : "")).catch(() => {});
|
||||
const whoProposed = (p) => (p.is_me ? "you" : p.username ? display(p.username) : `SoulStream ID ${fpShort(p.author)}`);
|
||||
const drawerOverlap = $derived(drawerUser ? overlapFor(drawerUser) : null);
|
||||
async function importPinned() {
|
||||
if (!sv?.pinned_uuid) return;
|
||||
const id = await shareImport({ id: "pin:" + sv.pinned_uuid, owner_key: sv.pinned_owner, playlist_uuid: sv.pinned_uuid, name: pinnedPreview?.name ?? "Pinned playlist" });
|
||||
@@ -1135,11 +1139,29 @@
|
||||
<h2 class="ax-title">Library overlap</h2>
|
||||
<span class="prov"><Icon name="library" size={12} /> computed on this device, after an explicit browse</span>
|
||||
</div>
|
||||
<div class="emptyshelf">
|
||||
<Icon name="folder" size={22} />
|
||||
<div><b>Nothing computed yet.</b> Browse a person's shares and SoulStream compares them with your library locally — nothing is uploaded to do it. The comparison itself is the next piece of this shelf.</div>
|
||||
<button class="ax-ghost sm" onclick={() => setSection("people")}>Browse someone</button>
|
||||
</div>
|
||||
{#if community.overlap.length}
|
||||
<div class="track">
|
||||
{#each community.overlap.slice(0, 24) as o (o.username)}
|
||||
<button class="pcard" onclick={() => (community.drawer = o.username)}>
|
||||
<span class="face" style:background="hsl({hueOf(o.username)} 30% 14%)"><Identicon seed={o.username} size={84} presence={presenceOf(o.username)} /></span>
|
||||
<span class="nm">{display(o.username)}</span>
|
||||
<span class="why">
|
||||
{#if o.albums_in_common_count}shares <b>{o.albums_in_common_count}</b> {o.albums_in_common_count === 1 ? "album" : "albums"} you own {#if o.gaps_count}· <b>{o.gaps_count}</b> you lack{/if}
|
||||
{:else if o.artists_in_common_count}<b>{o.artists_in_common_count}</b> {o.artists_in_common_count === 1 ? "artist" : "artists"} in common · <b>{o.gaps_count}</b> albums you lack
|
||||
{:else if o.their_albums}nothing in common · {fmtCount(o.their_albums)} albums browsed
|
||||
{:else}no album folders found{/if}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<p class="hint pad">Names compared, not files: a folder counts as yours when your library has that album under that artist. Browse someone again to refresh their line.</p>
|
||||
{:else}
|
||||
<div class="emptyshelf">
|
||||
<Icon name="folder" size={22} />
|
||||
<div><b>Nothing computed yet.</b> Browse a person's shares and SoulStream compares them with your library locally — nothing is uploaded to do it.</div>
|
||||
<button class="ax-ghost sm" onclick={() => setSection("people")}>Browse someone</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="hint wide">
|
||||
There is no global directory here and no "people you may know". Every shelf says where its people came from. Freenet
|
||||
@@ -1667,6 +1689,28 @@
|
||||
</div>
|
||||
<textarea class="note-in" rows="2" placeholder="A private note — only you see it" bind:value={noteDraft} onchange={() => saveMeta()}></textarea>
|
||||
|
||||
<div class="dsec">
|
||||
<h4>Library overlap <span class="src slsk"><Icon name="library" size={10} />this device</span></h4>
|
||||
{#if drawerOverlap}
|
||||
<p class="hint">
|
||||
{#if drawerOverlap.their_albums}<b>{drawerOverlap.albums_in_common_count}</b> of their {fmtCount(drawerOverlap.their_albums)} albums are in your library · <b>{drawerOverlap.artists_in_common_count}</b> {drawerOverlap.artists_in_common_count === 1 ? "artist" : "artists"} in common · <b>{drawerOverlap.gaps_count}</b> you lack{:else}No album folders among {fmtCount(drawerOverlap.audio_files)} audio files.{/if}
|
||||
· compared {fmtWhen(drawerOverlap.computed_at * 1000)}
|
||||
</p>
|
||||
{#if drawerOverlap.albums_in_common.length}
|
||||
<div class="ovl"><span class="lbl">In common</span>{#each drawerOverlap.albums_in_common.slice(0, 8) as a, i (i)}<span class="ov"><b>{a.album}</b> · {a.artist}</span>{/each}{#if drawerOverlap.albums_in_common_count > 8}<span class="ov more">+{drawerOverlap.albums_in_common_count - 8} more</span>{/if}</div>
|
||||
{/if}
|
||||
{#if drawerOverlap.gaps.length}
|
||||
<div class="ovl"><span class="lbl">You lack</span>{#each drawerOverlap.gaps.slice(0, 8) as a, i (i)}<span class="ov gap"><b>{a.album}</b> · {a.artist}</span>{/each}{#if drawerOverlap.gaps_count > 8}<span class="ov more">+{drawerOverlap.gaps_count - 8} more</span>{/if}</div>
|
||||
{/if}
|
||||
<div class="bbtns">
|
||||
<button class="ax-ghost sm" onclick={() => computeOverlap(drawerUser)} disabled={!online || !!community.busy["overlap:" + drawerUser]}><Icon name="refresh" size={12} /> {community.busy["overlap:" + drawerUser] ? "Browsing…" : "Compare again"}</button>
|
||||
<button class="lnk" onclick={() => forgetOverlap(drawerUser)} disabled={!!community.busy["overlap:" + drawerUser]}>forget</button>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="hint">Not compared yet. Their share folders against your albums, on this device — nothing is uploaded to do it.</p>
|
||||
<div class="bbtns"><button class="ax-ghost sm" onclick={() => computeOverlap(drawerUser)} disabled={!online || !!community.busy["overlap:" + drawerUser]}><Icon name="library" size={12} /> {community.busy["overlap:" + drawerUser] ? "Browsing their shares…" : "Compare with my library"}</button></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="dsec">
|
||||
<h4>Native Soulseek profile <span class="src slsk"><Icon name="globe" size={10} />Soulseek</span></h4>
|
||||
<dl class="kv">
|
||||
@@ -2045,6 +2089,13 @@
|
||||
.sphead .nm { font-size: 20px; font-weight: 800; display: flex; align-items: center; gap: 10px; }
|
||||
.sphead .sub { font-size: 12px; color: var(--text-muted); margin-top: 4px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; max-width: none; }
|
||||
.src.space { background: color-mix(in srgb, var(--space) 14%, transparent); color: #d9a6ff; }
|
||||
/* library overlap in the drawer */
|
||||
.ovl { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; align-items: center; }
|
||||
.ovl .lbl { font-size: 10px; font-weight: 900; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); margin-right: 4px; }
|
||||
.ovl .ov { font-size: 11px; padding: 3px 8px; border-radius: 99px; background: var(--surf-hi); color: var(--text-secondary); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ovl .ov b { color: var(--text-primary); font-weight: 700; }
|
||||
.ovl .ov.gap { background: color-mix(in srgb, var(--warn) 10%, transparent); }
|
||||
.ovl .ov.more { background: none; color: var(--text-muted); }
|
||||
/* the proposal board */
|
||||
.bbtns { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
|
||||
.dlist { list-style: none; margin: 10px 0; padding: 0; display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow: auto; }
|
||||
|
||||
Reference in new issue
Block a user