Files
SoulStream/handoff/rounds/ins-r38-screen.txt
T
KeelrLeek 9f0601027c android: the handoff — the pending rounds, the pipeline, and what is unverified
Everything written this session that has not been through the device: rounds 43
to 48 (each patch adversarially verified, 43's transient-notice expectation
fixed), a partial round 49, the pipeline scripts, the Android Auto design and
the icon comparison mock. Also the two harness changes that were never A/B'd —
adaptive waits, which may only shorten a declared wait and never lengthen it,
and a ship that runs the mock walk and the deploy at the same time. The A/B and
its 255-step baseline are in handoff/tools; main stays at what the phone has
actually walked.
2026-09-14 07:57:41 -05:00

197 lines
11 KiB
Plaintext

<script>
/* The whole discography — the desktop's CatalogDiscography.svelte, which
the artist page reaches with "All" on its Discography head.
The desktop's page is a stack of album-page heroes — the album page's
own hero at `--cover-scale: 0.45`, a 108px cover, with the record's
whole tracklist under it, fetched as the section scrolls near and its
accent sampled from its own cover (CatalogDiscography.svelte:120-175,
1555-1560). On a 411px screen that is one record per screenful and one
`catalog_album` per row over mobile data, for rows that are a tap away
on the record page — so none of those three comes over. What does is
the four things the page has that the artist page's tabs do not: All,
a sort, a grouping, and the editions kept whole.
Filed by the same classifier the artist page uses. The state — tab,
order, grouping — lives on the nav entry, so it survives a push and
the sort sheet's `patch()` reaches it the way Library's does. */
import { untrack } from "svelte";
import Icon from "../../components/Icon.svelte";
import Cover from "../Cover.svelte";
import { art, wantArt } from "../art.svelte.js";
import { albumsBy } from "../lib.svelte.js";
import { push, patch, openHalf } from "../nav.svelte.js";
import { catalog, lookupArtist, artistKey, yearOf, looseTitle, picture } from "../catalog.svelte.js";
import { discoTabs, allTabs, orderFamilies, byYear, ORDER_LABEL } from "../disco.js";
import { CAT_ONE } from "../../lib/localReleaseType.js";
import { editionLabel } from "../../lib/editions.js";
let { entry, screenEl = null } = $props();
const name = $derived(entry.c.name);
/* normally warm — the artist page that linked here looked the name up
already — but a page that only works warm is a page that breaks the
day something else links to it. `lookupArtist` is its own guard. */
$effect(() => { const n = name, sd = entry.c.seed ?? null; untrack(() => lookupArtist(n, sd)); });
const cat = $derived(catalog.artists[artistKey(name)] ?? null);
const page = $derived(cat?.page ?? null);
const artistId = $derived(page?.artist?.id ?? cat?.id ?? null);
const overview = $derived(artistId != null ? catalog.overviews[artistId] ?? null : null);
const shownName = $derived(page?.artist?.name || overview?.artist?.name || name);
/* the exhaustive page when it is here, the overview's first round until
then — never the other way round, the artist page's own rule */
const discoAll = $derived(page?.albums ?? overview?.albums ?? []);
/* ── the tabs, the sort, the grouping ────────────────────────────── */
const catTabs = $derived(discoTabs(discoAll));
const tabs = $derived(allTabs(catTabs));
const tab = $derived(tabs.some((t) => t.key === entry.c.tab) ? entry.c.tab : (tabs[0]?.key ?? "all"));
const order = $derived(entry.c.order || "new");
/* Under a name sort the years would repeat out of order, so By year is
not offered there and the grouping falls to none. The sheet says why. */
const byName = $derived(order === "az" || order === "za");
const grouped = $derived(!byName && (entry.c.group ?? "year") === "year");
const active = $derived(tabs.find((t) => t.key === tab) ?? null);
const items = $derived(orderFamilies(active?.items ?? [], order));
/* the desktop's header line: a record with no track count makes the
whole figure a floor, not a number */
const known = $derived(items.reduce((n, r) => n + (r.track_count || 0), 0));
const someUnknown = $derived(items.some((r) => r.track_count == null));
/* which category each family was filed under, read off the per-category
tabs rather than by asking the classifier again once per row */
const kinds = $derived.by(() => {
const m = new Map();
for (const t of catTabs) for (const f of t.items) m.set(f.familyKey ?? f.key ?? f.id, CAT_ONE[t.key] ?? t.label);
return m;
});
/* the cap. A prolific artist is hundreds of rows and the phone renders
every one of them if it is asked to — Browse's rule (Browse.svelte:51):
a slice that extends on demand, reset whenever the list underneath it
changes. It is taken BEFORE the grouping, so the last year on screen
can be a part of itself and the button says how much is left. */
const CAP = 60;
let cap = $state(CAP);
$effect(() => { void tab; void order; void grouped; cap = CAP; });
const groups = $derived(byYear(items.slice(0, cap), grouped));
/* ── what this phone already holds ───────────────────────────────── */
const albums = $derived(albumsBy(name));
const inLib = (a) => albums.find((x) => looseTitle(x.title) === looseTitle(a.title)) || null;
const familyLib = (f) => (f.editions || [f]).map(inLib).find(Boolean) || null;
const familyKeyOf = (f) => f.familyKey ?? f.key ?? f.id;
const subOf = (f, l) => [
yearOf(f.release_date),
kinds.get(familyKeyOf(f)) || "",
f.track_count ? `${f.track_count} tracks` : "",
l ? "in your library" : "",
].filter(Boolean).join(" · ");
const picUrl = $derived(picture(page?.artist?.picture) || picture(cat?.seed?.picture) || null);
const seedPath = $derived(albums.find((a) => a.artist_art_path)?.artist_art_path || albums[0]?.art_path || null);
$effect(() => { if (seedPath) wantArt(seedPath); });
const porSrc = $derived(picUrl || (seedPath ? art[seedPath] : "") || "");
/* ── opening things ──────────────────────────────────────────────── */
function openRow(r) {
const l = inLib(r);
if (l) push("album", { id: l.id }, screenEl);
else push("catalogAlbum", { id: r.id, artist: shownName, title: r.title }, screenEl);
}
function openFamily(f) {
const l = familyLib(f);
if (l) push("album", { id: l.id }, screenEl);
else push("catalogAlbum", { id: f.id, artist: shownName, title: f.title }, screenEl);
}
/* a long press is the record sheet the grid's already opens — Play, Save,
Add to playlist, Go to record, Go to artist. No new actions here. */
const moreFor = (f) => openHalf("catalogAlbum", { album: { ...(f.editions?.[0] ?? f), artist: shownName } });
/* the editions opened in place, under their own record — Browse's folder
and its files, rather than a rail that scrolls sideways inside a list
that scrolls down */
let openEds = $state(new Set());
function toggleEds(key) {
const s = new Set(openEds);
if (s.has(key)) s.delete(key); else s.add(key);
openEds = s;
}
</script>
<div class="dg-head">
<span class="dg-por">
{#if porSrc}<img src={porSrc} alt="" />{:else}<Icon name="users" size={22} />{/if}
</span>
<div class="b">
<div class="t">{shownName}</div>
<div class="m">{items.length} release{items.length === 1 ? "" : "s"} · {someUnknown ? "≥" : ""}{known} tracks</div>
</div>
</div>
{#if tabs.length > 1}
<div class="segtabs dg-tabs" role="tablist" aria-label="Discography">
{#each tabs as t (t.key)}
<button class:on={tab === t.key} onclick={() => patch({ tab: t.key })} role="tab" aria-selected={tab === t.key} aria-label={t.label + ", " + t.items.length}>{t.label}<span class="count" aria-hidden="true">{t.items.length}</span></button>
{/each}
</div>
{/if}
{#if items.length}
<div class="sec dg-sec">
<h2>{active?.label ?? "All"} <em>· {items.length} release{items.length === 1 ? "" : "s"}</em></h2>
<button class="all" onclick={() => openHalf("discosort")} aria-label="Sort and group">{ORDER_LABEL[order] ?? ORDER_LABEL.new}<Icon name="chevron-down" size={14} /></button>
</div>
{#each groups as g (g.key ?? "_all")}
{#if g.key}
<div class="dg-group">{g.key} <em>· {g.items.length} release{g.items.length === 1 ? "" : "s"}</em></div>
{/if}
{#each g.items as f (familyKeyOf(f))}
{@const l = familyLib(f)}
{@const key = familyKeyOf(f)}
{@const eds = (f.editions?.length ?? 1) > 1 ? f.editions : null}
<div class="dg-item">
<button class="row dg-row" onclick={() => openFamily(f)} oncontextmenu={(e) => { e.preventDefault(); moreFor(f); }}>
<span class="art">
{#if f.cover}<img src={f.cover} alt="" loading="lazy" />
{:else if l?.art_path}<Cover path={l.art_path} size={20} />
{:else}<Icon name="disc" size={20} />{/if}
</span>
<span class="b">
<span class="t">{f.title}</span>
<span class="m">{subOf(f, l)}</span>
</span>
</button>
{#if eds}
<button class="dg-eline" onclick={() => toggleEds(key)} aria-expanded={openEds.has(key)}>
<Icon name={openEds.has(key) ? "chevron-up" : "chevron-down"} size={13} />{openEds.has(key) ? "Hide editions" : `${eds.length} editions`}
</button>
{#if openEds.has(key)}
<div class="eds dg-eds">
{#each eds as e, i (e.id)}
<button class="ed" onclick={() => openRow(e)}>
<span class="ed-art">{#if e.cover}<img src={e.cover} alt="" loading="lazy" />{:else}<Icon name="disc" size={16} />{/if}</span>
<span class="ed-copy">
<span class="n">{i === 0 ? "Original" : editionLabel(e, f)}</span>
<span class="y">{[yearOf(e.release_date), e.track_count ? `${e.track_count} tracks` : ""].filter(Boolean).join(" · ")}</span>
</span>
</button>
{/each}
</div>
{/if}
{/if}
</div>
{/each}
{/each}
{#if items.length > cap}
<div style="padding: 10px var(--pad) 4px"><button class="btn btn-ghost wide dg-more" onclick={() => (cap += 120)}>+ {items.length - cap} more releases</button></div>
{/if}
{:else if !cat || cat.status === "loading"}
<div class="sec-note" style="padding-top: 16px">Asking the catalog…</div>
{:else}
<div class="stub"><b>Nothing to list.</b>The catalog has no releases for {shownName}{cat.status === "error" ? " — it did not answer" : ""}.</div>
{/if}
<div class="endpad"></div>