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:
KeelrLeek committed 2026-09-08 12:10:49 -05:00
1 parent a98c750908
commit b8e6fb7396
11 files changed
+454 -6

No files matched your search

+25 -4
View File
@@ -19,7 +19,7 @@
// is CatalogArtist's class for class with its animation numbers unchanged.
// Styles are copied because Svelte scopes them per component — the same
// trade CatalogDiscography made, with provenance comments at each block.
import { tick } from "svelte";
import { tick, untrack } from "svelte";
import { fade } from "svelte/transition";
import Icon from "../components/Icon.svelte";
import PlaylistMenu from "../components/PlaylistMenu.svelte";
@@ -41,6 +41,7 @@
importResume,
goToView,
} from "../lib/state.svelte.js";
import { openWhoHas, holdersFor, pairKey, holderCount } from "../lib/whoHas.svelte.js";
import {
fileRelease,
CAT_ORDER,
@@ -121,6 +122,17 @@
} catch { return {}; }
})());
let gapRows = $state({}); // foldKey(artist) -> catalog rows not owned
let gapNames = $state({}); // foldKey(artist) -> the artist's display name
// Who in your rooms holds a gap: asked for every open shelf's rows at
// once, filed by pairKey. A marked card opens the ask sheet.
let heldBy = $state({});
$effect(() => {
const items = Object.keys(gapOpen)
.filter((k) => gapOpen[k] && gapRows[k]?.length && gapNames[k])
.flatMap((k) => gapRows[k].map((r) => ({ artist: gapNames[k], album: r.title })));
if (!items.length) return;
untrack(() => void holdersFor(items).then((m) => { for (const [k, v] of m) heldBy[k] = v; }));
});
let gapOpen = $state({}); // foldKey(artist) -> line expanded
let gapLoading = $state({});
@@ -182,6 +194,7 @@
...gapRows,
[key]: released.filter((r) => !ownedTitles.has(normKey(r.title))),
};
gapNames = { ...gapNames, [key]: artistName };
gapTotals = { ...gapTotals, [key]: { total: released.length } };
try { localStorage.setItem(GAPS_KEY, JSON.stringify(gapTotals)); } catch { /* ignore */ }
} catch { /* the line stays collapsed; nothing is invented */ }
@@ -1544,7 +1557,7 @@
</div>
{/snippet}
{#snippet gapCard(r)}
{#snippet gapCard(r, artist)}
<!-- A record you do NOT own, at full size and dimmed. It is filed by the
same classifier as everything else, off the CatalogAlbum payload's own
fields (`record_type`/`track_count`/`release_date`, plus the is_live and
@@ -1558,7 +1571,8 @@
is_live: r.is_live,
is_soundtrack: r.is_soundtrack,
})}
<div class="release gap" role="listitem" title={`${r.title} — not in your library`}>
{@const held = heldBy[pairKey(artist, r.title)]}
<div class="release gap" class:held={!!held?.length} role="listitem" title={`${r.title} — not in your library`}>
<div class="cover-wrap">
{#if r.release_date?.slice(0, 4)}
<span class="ed-badge">{r.release_date.slice(0, 4)}</span>
@@ -1568,6 +1582,9 @@
{:else}
<div class="cover placeholder"><Icon name="disc" size={30} /></div>
{/if}
{#if held?.length}
<button class="have" title="{holderCount(held)} in your rooms {holderCount(held) === 1 ? "has" : "have"} this — who?" onclick={(e) => { e.stopPropagation(); openWhoHas({ artist, album: r.title, cover: r.cover ?? null }, e.currentTarget.closest(".release")); }}>{holderCount(held)} {holderCount(held) === 1 ? "has it" : "have it"}</button>
{/if}
</div>
<div class="title" title={r.title}>{r.title}</div>
<div class="sub">{cat === "other" ? "" : CAT_ONE[cat]}</div>
@@ -1826,7 +1843,7 @@
<div class="lib-gaps" role="list">
<div class="grid">
{#each missing as r (r.id)}
{@render gapCard(r)}
{@render gapCard(r, name)}
{/each}
</div>
</div>
@@ -2942,6 +2959,10 @@
.lib-gaps { margin-top: 12px; }
.lib-gaps .release.gap { opacity: 0.44; transition: opacity 150ms ease; cursor: default; }
.lib-gaps .release.gap:hover { opacity: 1; background: none; }
.lib-gaps .release.gap.held { opacity: 0.72; }
/* who in your rooms holds a release you lack — the count rides the card */
.have { position: absolute; right: 6px; bottom: 6px; z-index: 2; display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 7px; border-radius: 99px; background: rgba(0, 0, 0, 0.62); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); color: #fff; font-size: 10.5px; font-weight: 900; border: 1px solid rgba(255, 255, 255, 0.14); cursor: pointer; white-space: nowrap; }
.have:hover { background: rgba(0, 0, 0, 0.82); border-color: rgba(255, 255, 255, 0.34); }
/* ============================================================ the table
The power-user surface: every column is something the files carry. The