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.
52 lines
2.2 KiB
Plaintext
52 lines
2.2 KiB
Plaintext
/** The rows a record's own artist anchor found no count for — a feature's
|
||
* guest, a compilation's stranger — asked by each row's OWN Deezer artist
|
||
* id, the rule the desktop's search results follow. A row the backend has
|
||
* no count for keeps nothing: unknown is not a zero. */
|
||
export function loadTrackListeners(albumId, tracks) {
|
||
if (albumId == null || catalog.listeners[albumId]) return;
|
||
const want = (tracks || []).filter((t) => t.listeners == null && t.artist_id != null);
|
||
if (!want.length) return;
|
||
catalog.listeners[albumId] = { status: "loading", counts: {} };
|
||
api.catalogTrackListeners(want.map((t) => ({ artistId: t.artist_id, title: t.title }))).then(
|
||
(counts) => {
|
||
const out = {};
|
||
(counts || []).forEach((n, i) => { if (n != null && want[i]) out[want[i].id] = n; });
|
||
catalog.listeners[albumId] = { status: "ready", counts: out };
|
||
},
|
||
() => { catalog.listeners[albumId] = { status: "error", counts: {} }; },
|
||
);
|
||
}
|
||
|
||
/** Forget an artist's page and ask for it again — the desktop's
|
||
* `invalidateCatalogArtist`, for a hero that has just changed. The seed is
|
||
* kept, so the identity search does not run a second time. */
|
||
export function forgetArtist(name) {
|
||
const k = artistKey(name);
|
||
const cur = catalog.artists[k];
|
||
if (!cur) return;
|
||
delete catalog.artists[k];
|
||
lookupArtist(name, cur.seed ?? null);
|
||
}
|
||
|
||
/** A photo from this phone as an artist's hero — the desktop's Choose hero,
|
||
* through the phone's own image picker. The page is asked again, so the
|
||
* banner, the ground and the accent all follow. */
|
||
export async function heroFromPhoto(id, name) {
|
||
let r;
|
||
try { r = await api.pickImage(); } catch (e) { if (/cancelled/.test(String(e))) return false; throw e; }
|
||
if (!r?.path) return false;
|
||
const hero = await api.setArtistHero(id, r.path);
|
||
forgetArtist(name);
|
||
note(`Hero set · ${hero.width}×${hero.height}`);
|
||
return true;
|
||
}
|
||
|
||
/** The chosen hero removed — the catalog's own photos come back. */
|
||
export async function heroClear(id, name) {
|
||
const removed = await api.clearArtistHero(id);
|
||
forgetArtist(name);
|
||
note(removed ? "The catalog's own photos are back" : "No custom hero here");
|
||
}
|
||
|
||
export function lookupAlbum(id) {
|