Files
SoulStream/src/mobile/disco.js
T
KeelrLeek f2cde4ec7c android: the whole discography, on a page of its own
The artist page already filed every release the desktop's way, but it only
ever showed one category at a time and it showed all of it: no slice anywhere
in that grid, so an artist with 140 releases drew 140 cards. There is a page
now — All in front of the categories, a sort, a grouping by year, and the
editions opened in place under their own record, which is the part a flat
list loses — and the grid behind it stops at twelve with the way down under
it. It is the same classifier twice over the same cached albums, which the
desktop cannot do because its filing waits on a background description
harvest; the three things its own page has and this one does not — a
tracklist per record, an accent sampled per cover, the scroll-led banner —
are 140 calls and 140 decodes over mobile data for rows that are one tap
away, and ANDROID.md says so rather than leaving them looking forgotten.

The list caps at sixty with the rest a button away, before the grouping, so
the last year on screen can be a part of itself; under a name order By year
is not offered, because the years would repeat out of sequence. The mock
gains a 140-record artist so both caps are walked rather than argued about.
2026-09-14 03:41:16 -05:00

153 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* The discography as the desktop files it (CatalogArtist.svelte's tabs):
one category per release, the categories' order and labels from the
shared TAB_DEFS, editions folded into families by editions.js. The
desktop also weighs MusicBrainz type hints and each record's own
description before the title heuristics; the phone files on what the
catalog page carries — the edition link, the announced date, the
mixtape flag, then the same title rules the library uses. */
import { categoryForLocal, TAB_DEFS } from "../lib/localReleaseType.js";
import { releaseFamilies } from "../lib/editions.js";
const today = () => new Date().toISOString().slice(0, 10);
/** An announced full-length: dated, and the date is still ahead. */
export const isUpcoming = (a) =>
(a.record_type || "").toLowerCase() === "album"
&& typeof a.release_date === "string" && a.release_date.length >= 10
&& a.release_date.slice(0, 10) > today();
function ownCategory(album) {
if (isUpcoming(album)) return "upcoming";
if (album.is_mixtape) return "mixtape";
return categoryForLocal(album);
}
/** A linked edition files WITH its base release (one hop). */
export function categoryFor(album, albums) {
if (album.edition_group != null && album.id !== album.edition_group) {
const base = (albums || []).find((o) => o.id === album.edition_group);
if (base) return ownCategory(base);
}
return ownCategory(album);
}
/** [{ key, label, items: families }] — only the tabs with something in them. */
export function discoTabs(albums) {
const list = albums || [];
const by = {};
for (const def of TAB_DEFS) by[def.key] = [];
for (const a of list) (by[categoryFor(a, list)] ?? by.other).push(a);
return TAB_DEFS
.map((def) => ({ key: def.key, label: def.label, items: releaseFamilies(def.cats.flatMap((c) => by[c] || []), def.key) }))
.filter((t) => t.items.length > 0);
}
/** "1997–present" / "1997–2013": the released records' span. */
export function activeYears(albums) {
const years = (albums || [])
.filter((a) => !isUpcoming(a))
.map((a) => Number(String(a.release_date || "").slice(0, 4)))
.filter((y) => Number.isFinite(y) && y > 1900);
if (!years.length) return null;
const first = Math.min(...years);
const last = Math.max(...years);
const nowYear = new Date().getFullYear();
return last >= nowYear - 1 ? `${first}–present` : `${first}–${last}`;
}
/** The newest dated record. */
export function latestOf(albums) {
let best = null;
for (const a of albums || []) {
if (!a.release_date) continue;
if (!best || a.release_date > best.release_date) best = a;
}
return best;
}
/** One column per year across the whole span, gaps included — null under
* three years, when it would say nothing. */
export function timelineOf(albums) {
const byYear = new Map();
for (const a of albums || []) {
const y = String(a.release_date || "").slice(0, 4);
if (y) byYear.set(y, (byYear.get(y) ?? 0) + 1);
}
const years = [...byYear.keys()].sort();
if (years.length < 3) return null;
const first = Number(years[0]);
const last = Number(years[years.length - 1]);
const peak = Math.max(...byYear.values());
const cols = [];
for (let y = first; y <= last; y++) {
const n = byYear.get(String(y)) ?? 0;
cols.push({ year: y, n, h: n ? Math.max(8, Math.round((n / peak) * 44)) : 2 });
}
return { cols, first, last, total: (albums || []).length };
}
/** The desktop's social chips: a label per link kind. */
export const SOCIAL_LABEL = {
website: "Website", wikipedia: "Wikipedia", bandcamp: "Bandcamp", soundcloud: "SoundCloud",
youtube: "YouTube", instagram: "Instagram", twitter: "X", x: "X", facebook: "Facebook", tiktok: "TikTok",
spotify: "Spotify", apple: "Apple Music", discogs: "Discogs", lastfm: "Last.fm", musicbrainz: "MusicBrainz",
};
/* ── the whole discography, on a page of its own ─────────────────────────
The artist page's tabs are one lane per category; the discography page is
the same filing seen whole. Both read ONE classifier, and it is this one.
The desktop cannot do that: CatalogDiscography.svelte takes the artist
page's already-computed tab list as the same array object, because the
desktop's filing depends on a background description harvest and on
deferred MusicBrainz type hints that a re-run would not read
(CatalogDiscography.svelte:28-46). `discoTabs` here is pure and reads only
what the catalog page carries, so the phone simply calls it twice over the
same cached albums and the two pages can never disagree. */
/** "All" in front of the categories — built by CONCATENATING the per-category
* families, never by re-folding the whole set. A single that shares an
* album's base title (Pond released "Terrestrials" in 2026 as an album AND
* as a single) would fold into it as a phantom edition, and one of the two
* would vanish from its own tab. */
export function allTabs(tabs) {
const ts = tabs || [];
if (!ts.length) return [];
return [{ key: "all", label: "All", items: ts.flatMap((t) => t.items) }, ...ts];
}
/** The four orders the page offers, and what each is called. The sort sheet
* in HalfSheet.svelte writes the same four out inline — it is edited by
* every other round and a new import there is a collision waiting to
* happen — so if a label changes it changes in both places. */
export const ORDER_LABEL = { new: "Newest first", old: "Oldest first", az: "A–Z", za: "Z–A" };
/** The desktop's sort (CatalogDiscography.svelte:71-78): dates on
* `release_date`, names on the title, either direction. */
export function orderFamilies(items, order) {
const list = [...(items || [])];
const byName = order === "az" || order === "za";
list.sort((a, b) => (byName
? (a.title ?? "").localeCompare(b.title ?? "")
: (a.release_date ?? "").localeCompare(b.release_date ?? "")));
return order === "new" || order === "za" ? list.reverse() : list;
}
/** [{ key: "2001", items }] — the year headers, cut out of the list in the
* order it is already in rather than re-sorted, which is why a name order
* must not be grouped: the years would repeat out of sequence. `on` false
* gives the whole list back under one null key, so the caller renders one
* loop either way. */
export function byYear(items, on) {
const list = items || [];
if (!on) return list.length ? [{ key: null, items: list }] : [];
const out = [];
for (const r of list) {
const y = String(r.release_date || "").slice(0, 4) || "—";
const last = out[out.length - 1];
if (last && last.key === y) last.items.push(r);
else out.push({ key: y, items: [r] });
}
return out;
}