android: the catalog's numbers — Popular's own lane, a count per row, the artist's photo
Popular is asked on its own as well as with the page, so the section can paint before the full page is built and an artist whose page came back with none still gets one. A catalog record's rows that the album's own artist anchor found no count for — a feature's guest, a compilation's stranger — get the deferred Last.fm pass by their own artist id. The artist page's ⋯ is its own sheet now: choose a hero photo from this phone, or put the catalog's own back; set_artist_hero is pick_artist_hero's install step split out and shared, so the desktop's dialog keeps its door. You gains the desktop's Refresh artist portraits, and a data: URL is the image itself in the art gate — a chosen hero comes back from Rust as one, and the thumbnail fetch used to throw it away.
This commit is contained in:
1 parent
c737809325
commit
1737c5e1b6
12 files changed
+232
-13
No files matched your search
@@ -403,6 +403,9 @@ export const catalogArtistPopular = (artistId) =>
|
||||
invoke("catalog_artist_popular", { artistId });
|
||||
export const pickArtistHero = (artistId) => invoke("pick_artist_hero", { artistId });
|
||||
export const clearArtistHero = (artistId) => invoke("clear_artist_hero", { artistId });
|
||||
// The phone's hero door: a photo the storage plugin already copied into the
|
||||
// app's cache, installed the way the desktop's dialog pick installs one.
|
||||
export const setArtistHero = (artistId, path) => invoke("set_artist_hero", { artistId, path });
|
||||
export const catalogRelated = (artistId) => invoke("catalog_related", { artistId });
|
||||
// Deferred Last.fm pass for search results: `tracks` is [{ artistId, title }]
|
||||
// and the answer comes back in the same order. Kept OUT of catalog_search,
|
||||
|
||||
+31
-2
@@ -370,6 +370,9 @@ export function mockInvoke(cmd, args) {
|
||||
if (cmd === "album_tracks")
|
||||
return new Promise((r) => setTimeout(() => r(tracksFor(args.albumId)), 60));
|
||||
if (cmd === "album_art") {
|
||||
/* not a path: the real one fails to open it, which is why the art
|
||||
gate has to answer a data: URL itself */
|
||||
if (String(args.path || "").startsWith("data:")) return Promise.reject(new Error("album_art: not a path"));
|
||||
const m = /(\d+)$/.exec(args.path || "");
|
||||
const num = m ? +m[1] : 0;
|
||||
return Promise.resolve(svgArt((num * 47) % 360, String.fromCharCode(65 + (num % 26))));
|
||||
@@ -494,6 +497,8 @@ const NEWS = [
|
||||
{ title: "Infinity Repeating gets a video", url: "https://example.com/news/2", source: "NME", date: Math.floor(Date.now() / 1000) - 86400 * 2 },
|
||||
];
|
||||
const RELATED = [["Justice", 411], ["Air", 412], ["Kavinsky", 413], ["Chemical Brothers", 414]].map(([name, id], i) => ({ id, name, picture: i % 2 ? null : svgArt(40 + id, name[0]), album_count: 3, fans: 900000 - id * 100 }));
|
||||
/* the heroes chosen this session — pick_image → set_artist_hero */
|
||||
const heroes = {};
|
||||
let follows = [];
|
||||
function mockCatalog(cmd, args) {
|
||||
if (cmd === "catalog_artist") {
|
||||
@@ -502,7 +507,7 @@ function mockCatalog(cmd, args) {
|
||||
if (!a) return Promise.reject(new Error("catalog_artist: unknown artist"));
|
||||
const own = a === CAT_ARTIST;
|
||||
return new Promise((r) => setTimeout(() => r({
|
||||
artist: a, banner: null, banners: [], banner_custom: false, logos: [],
|
||||
artist: a, banner: heroes[a.id] ?? null, banners: heroes[a.id] ? [heroes[a.id]] : [], banner_custom: !!heroes[a.id], logos: [],
|
||||
popular_tracks: own ? CAT_TRACKS.slice(0, 7).map((t, i) => ({ ...t, listeners: 2_400_000 - i * 310_000 })) : [],
|
||||
albums: own ? CAT_DISCO : [{ id: 900 + id, title: a.name + " Live", artist: a.name, cover: null, release_date: "2015-01-01", record_type: "album", track_count: 9, edition_group: null, is_soundtrack: false }],
|
||||
top_genre: "Electronic", socials: [],
|
||||
@@ -518,6 +523,27 @@ function mockCatalog(cmd, args) {
|
||||
if (cmd === "catalog_artist_profile") return Promise.resolve({ listeners: 3_100_000, plays: 210_000_000, tags: ["electronic", "house", "french house", "dance"] });
|
||||
if (cmd === "catalog_artist_bio") return Promise.resolve("Daft Punk were a French electronic music duo formed in Paris in 1993 by Guy-Manuel de Homem-Christo and Thomas Bangalter. They achieved popularity in the late 1990s as part of the French house movement.");
|
||||
if (cmd === "catalog_related") return Promise.resolve(RELATED);
|
||||
/* Popular's own lane: ranked, with counts — and for the artists whose
|
||||
page gives none (the related ones), the only Popular there is */
|
||||
if (cmd === "catalog_artist_popular") {
|
||||
const id = Number(args?.artistId);
|
||||
const a = id === CAT_ARTIST.id ? CAT_ARTIST : RELATED.find((r) => r.id === id);
|
||||
if (!a) return Promise.resolve([]);
|
||||
const rows = id === CAT_ARTIST.id
|
||||
? CAT_TRACKS.slice(0, 7)
|
||||
: TITLES.slice(0, 4).map((t, i) => ({
|
||||
id: 4200000 + id * 10 + i, title: t, artist: a.name, artist_id: id, rank: 700000 - i * 1000,
|
||||
album: a.name + " Live", album_id: 900 + id, cover: null, duration_secs: 210 + i * 13, preview: null, track_no: i + 1,
|
||||
}));
|
||||
return new Promise((r) => setTimeout(() => r(rows.map((t, i) => ({ ...t, listeners: 1_900_000 - i * 240_000 }))), 260));
|
||||
}
|
||||
/* the deferred pass: a count per row, by that row's own artist id */
|
||||
if (cmd === "catalog_track_listeners") {
|
||||
const rows = args?.tracks || [];
|
||||
return new Promise((r) => setTimeout(() => r(rows.map((t, i) => (t.artistId ? 640_000 - i * 20_000 : null))), 260));
|
||||
}
|
||||
if (cmd === "set_artist_hero") { const id = Number(args?.artistId); heroes[id] = svgArt(180, "H"); return Promise.resolve({ data_url: heroes[id], width: 3840, height: 2160 }); }
|
||||
if (cmd === "clear_artist_hero") { const id = Number(args?.artistId); const had = id in heroes; delete heroes[id]; return Promise.resolve(had); }
|
||||
if (cmd === "artist_play_stats") return Promise.resolve({ seconds: 7380, last_played_at: Math.floor(Date.now() / 1000) - 86400, top_title: "Track 1", top_count: 12 });
|
||||
if (cmd === "followed_artists") return Promise.resolve(follows.slice());
|
||||
if (cmd === "follow_artist") { follows.push({ artist_id: args?.artistId, name: args?.name, portrait: args?.portrait ?? null, followed_at: Date.now() / 1000 }); later(30, () => mockEmit("soulseek:follows-changed", {})); return Promise.resolve(); }
|
||||
@@ -532,7 +558,9 @@ function mockCatalog(cmd, args) {
|
||||
if (cmd === "catalog_album") {
|
||||
const a = CAT_ALBUMS.find((x) => x.id === Number(args?.albumId));
|
||||
if (!a) return Promise.reject(new Error("catalog_album: unknown album"));
|
||||
return Promise.resolve({ album: a, tracks: a.id === 302127 ? CAT_TRACKS.map((t, i) => ({ ...t, listeners: 2_400_000 - i * 150_000 })) : [], album_plays: a.id === 302127 ? 310_000_000 : null, album_listeners: a.id === 302127 ? 4_100_000 : null, totals_summed: false });
|
||||
/* the last two rows are a guest's: the album's own artist anchor
|
||||
has no count for them, which is what the deferred pass is for */
|
||||
return Promise.resolve({ album: a, tracks: a.id === 302127 ? CAT_TRACKS.map((t, i) => (i >= CAT_TRACKS.length - 2 ? { ...t, artist: "Daft Punk feat. Todd Edwards", artist_id: 411 } : { ...t, listeners: 2_400_000 - i * 150_000 })) : [], album_plays: a.id === 302127 ? 310_000_000 : null, album_listeners: a.id === 302127 ? 4_100_000 : null, totals_summed: false });
|
||||
}
|
||||
if (cmd === "catalog_album_info") {
|
||||
const t = String(args?.title || "");
|
||||
@@ -558,6 +586,7 @@ function mockCatalog(cmd, args) {
|
||||
if (cmd === "rescan_library") return Promise.resolve();
|
||||
if (cmd === "library_art_prewarm") return Promise.resolve();
|
||||
if (cmd === "refresh_all_album_art") return new Promise((r) => setTimeout(() => r(140), 300));
|
||||
if (cmd === "refresh_artist_art") return new Promise((r) => setTimeout(() => r(26), 300));
|
||||
if (cmd === "catalog_art") return Promise.resolve(svgArt(200, "D"));
|
||||
if (cmd === "stream_cache_ready") return Promise.resolve(cacheReady.has(args?.path) || (xfer && xfer.path === args?.path && xfer.received >= xfer.total));
|
||||
if (cmd === "preload_stream") {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import { nav, closeHalf, push, top, patch, go, back, openHalf, openSheet } from "./nav.svelte.js";
|
||||
import { player, playNext, enqueue, playEntries, playQueueAt, removeFromQueue, moveToNext, moveInQueue, setSleep, sleepLabel } from "./player.svelte.js";
|
||||
import { streamSong, saveSong, streamCatalogAlbum, saveCatalogAlbum } from "./resolve.svelte.js";
|
||||
import { catalog, yearOf } from "./catalog.svelte.js";
|
||||
import { catalog, yearOf, artistKey, picture, heroFromPhoto, heroClear } from "./catalog.svelte.js";
|
||||
import { pls, bulk, bulkable, runBulk, coverFor, queueable, entryOf, playFrom, shufflePlay, togglePin, renamePlaylist, describePlaylist, duplicatePlaylist, deletePlaylist, removeItems, addEntries, createPlaylist, moveItem, coverFromPhoto, coverClear } from "./playlists.svelte.js";
|
||||
import { DOORS, start as startImport, pickImportFolder, startFolder } from "./importer.svelte.js";
|
||||
import { slsk, refreshRooms, disconnect, openThread, addBuddy, removeBuddy, leaveRoom } from "./slsk.svelte.js";
|
||||
@@ -38,6 +38,7 @@
|
||||
if (t.n === "library") return "library-more";
|
||||
if (t.n === "playlist") return "playlist-more";
|
||||
if (t.n === "catalogAlbum") return "catalogAlbum-more";
|
||||
if (t.n === "artist") return "artist-more";
|
||||
return "home-more";
|
||||
});
|
||||
|
||||
@@ -682,6 +683,25 @@
|
||||
<div class="sheet-hair"></div>
|
||||
{@render row("refresh", "Rescan library", "looks for new files in the folder", act(rescan))}
|
||||
</div>
|
||||
{:else if kind === "artist-more"}
|
||||
{@const nm = top().c.name}
|
||||
{@const ent = catalog.artists[artistKey(nm)] ?? null}
|
||||
{@const pg = ent?.page ?? null}
|
||||
{@const aid = pg?.artist?.id ?? ent?.id ?? null}
|
||||
<div class="sheet-head">
|
||||
<span class="art">{#if picture(pg?.artist?.picture)}<img src={picture(pg.artist.picture)} alt="" />{:else}<Icon name="users" size={22} />{/if}</span>
|
||||
<span class="b"><span class="t">{nm}</span><span class="m">{pg?.albums?.length ? pg.albums.length + " releases on the catalog" : "Artist"}</span></span>
|
||||
</div>
|
||||
<div class="sheet-rows">
|
||||
{#if aid != null}
|
||||
{@render row("image", "Choose hero photo…", "a wide picture from this phone, 1920×800 or larger", act(() => heroFromPhoto(aid, nm).catch(oops)))}
|
||||
{#if pg?.banner_custom}
|
||||
{@render row("x", "Remove custom hero", "the catalog's own photos come back", act(() => heroClear(aid, nm).catch(oops)))}
|
||||
{/if}
|
||||
<div class="sheet-hair"></div>
|
||||
{/if}
|
||||
{@render row("settings", "Artwork settings", "covers and portraits, in You", act(() => go("you")))}
|
||||
</div>
|
||||
{:else if kind === "home-more"}
|
||||
<div class="sheet-title">SoulStream</div>
|
||||
<div class="sheet-rows">
|
||||
|
||||
@@ -23,6 +23,9 @@ let inFlight = 0;
|
||||
export function loadArt(path, urgent = false) {
|
||||
if (!path) return Promise.resolve("");
|
||||
if (art[path] !== undefined) return Promise.resolve(art[path]);
|
||||
/* a data: URL is already the image — an artist's chosen hero comes back
|
||||
from Rust as one, and the thumbnail fetch would throw it away */
|
||||
if (path.startsWith("data:")) { art[path] = path; return Promise.resolve(path); }
|
||||
/* a streamed record's cover comes out of the file's own tags once enough
|
||||
of it is here (player.svelte.js sets it); nothing to fetch, and a miss
|
||||
must not be remembered */
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
exact-name matches, the most-followed wins, so a shared name resolves to
|
||||
the canonical artist and never to a namesake the search listed first. */
|
||||
import * as api from "../lib/api.js";
|
||||
import { note } from "./notice.svelte.js";
|
||||
|
||||
export const catalog = $state({
|
||||
/** lowercase name → { status: loading | ready | none | error, id, seed, page, error } */
|
||||
@@ -23,6 +24,11 @@ export const catalog = $state({
|
||||
seeds: {},
|
||||
/** "artist\u0000title" → { status, info: { description, genre, label, source } } */
|
||||
albumInfo: {},
|
||||
/** artist id → { status, tracks } — Popular on its own, ahead of the page */
|
||||
popular: {},
|
||||
/** catalog album id → { status, counts: { trackId: listeners } } — the
|
||||
* deferred Last.fm pass for the rows the record's page had no count for */
|
||||
listeners: {},
|
||||
});
|
||||
|
||||
export function artistKey(name) {
|
||||
@@ -91,6 +97,20 @@ export function loadProfile(id) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Popular on its own — the desktop's fast lane (`catalog_artist_popular`).
|
||||
* It waits on nothing but the linked MBID and one Last.fm call, so the
|
||||
* section can paint while the full page is still being built; the page's
|
||||
* own list wins once it lands (the same order, from the same inputs), and
|
||||
* this one stays for an artist whose page came back with no Popular. */
|
||||
export function loadPopular(id) {
|
||||
if (id == null || catalog.popular[id]) return;
|
||||
catalog.popular[id] = { status: "loading", tracks: [] };
|
||||
api.catalogArtistPopular(id).then(
|
||||
(tracks) => { catalog.popular[id] = { status: "ready", tracks: tracks ?? [] }; },
|
||||
() => { catalog.popular[id] = { status: "error", tracks: [] }; },
|
||||
);
|
||||
}
|
||||
|
||||
/** A portrait for a byline — the same identity rule, search only. */
|
||||
export function seedFor(name) {
|
||||
const k = artistKey(name);
|
||||
@@ -114,6 +134,56 @@ export function loadAlbumInfo(artist, title) {
|
||||
);
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
if (id == null) return;
|
||||
const cur = catalog.albums[id];
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { art, wantArt } from "../art.svelte.js";
|
||||
import { player, playEntries, playAlbum } from "../player.svelte.js";
|
||||
import { push, openHalf } from "../nav.svelte.js";
|
||||
import { catalog, lookupArtist, artistKey, fans, yearOf, looseTitle, loadExtras, picture } from "../catalog.svelte.js";
|
||||
import { catalog, lookupArtist, artistKey, fans, yearOf, looseTitle, loadExtras, loadPopular, picture } from "../catalog.svelte.js";
|
||||
import { resolver, streamSong, streamCatalogAlbum } from "../resolve.svelte.js";
|
||||
import { discoTabs, activeYears, latestOf, timelineOf, isUpcoming, SOCIAL_LABEL } from "../disco.js";
|
||||
import { shuffled, mmss, ago } from "../util.js";
|
||||
@@ -64,6 +64,10 @@
|
||||
const nm = page?.artist?.name || name;
|
||||
if (id != null) untrack(() => loadExtras(id, nm));
|
||||
});
|
||||
/* Popular's own lane, asked the moment an id is known — from the
|
||||
search's own row, or from the identity search — so the section can
|
||||
paint before the full page is built */
|
||||
$effect(() => { const id = artistId; if (id != null) untrack(() => loadPopular(id)); });
|
||||
const seedSrc = $derived(seedPath ? art[seedPath] : "");
|
||||
const heroSrc = $derived(heroUrl ? art[heroUrl] : "");
|
||||
const extras = $derived(artistId != null ? catalog.extras[artistId] ?? null : null);
|
||||
@@ -88,7 +92,11 @@
|
||||
});
|
||||
|
||||
/* ── Popular ─────────────────────────────────────────────────────── */
|
||||
const popularAll = $derived(page?.popular_tracks ?? []);
|
||||
/* the page's own list once it lands (the same Last.fm order, from the
|
||||
same inputs); the fast lane's until then, and for an artist whose
|
||||
page came back with no Popular at all */
|
||||
const fastPopular = $derived(artistId != null ? catalog.popular[artistId]?.tracks ?? [] : []);
|
||||
const popularAll = $derived(page?.popular_tracks?.length ? page.popular_tracks : fastPopular);
|
||||
let showAllPopular = $state(false);
|
||||
const popular = $derived((showAllPopular ? popularAll : popularAll.slice(0, 5)).map((t) => ({ t, hit: libByTitle.get(looseTitle(t.title)) || null })));
|
||||
const popPeak = $derived(popularAll.reduce((m, t) => Math.max(m, t.listeners || 0), 0));
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import { albumsBy, tracksOf } from "../lib.svelte.js";
|
||||
import { player, playAlbum, playAlbumShuffled, toggleShuffle } from "../player.svelte.js";
|
||||
import { push, openHalf } from "../nav.svelte.js";
|
||||
import { catalog, lookupAlbum, lookupArtist, artistKey, seedFor, picture, yearOf, looseTitle, loadAlbumInfo, infoKey } from "../catalog.svelte.js";
|
||||
import { catalog, lookupAlbum, lookupArtist, artistKey, seedFor, picture, yearOf, looseTitle, loadAlbumInfo, infoKey, loadTrackListeners } from "../catalog.svelte.js";
|
||||
import { resolver, streamSong, streamCatalogAlbum, saveCatalogAlbum } from "../resolve.svelte.js";
|
||||
import { releaseFamilies, editionLabel, splitDescription } from "../../lib/editions.js";
|
||||
import { mmss, longDur } from "../util.js";
|
||||
@@ -70,7 +70,14 @@
|
||||
const total = $derived(rows.reduce((n, t) => n + (t.duration_secs || 0), 0));
|
||||
const runtime = $derived(total ? longDur(total) : "");
|
||||
const multiDisc = $derived(new Set(rows.map((t) => t.disk_number ?? 1)).size > 1);
|
||||
const peak = $derived(rows.reduce((m, t) => Math.max(m, t.listeners || 0), 0));
|
||||
/* the rows the record's own artist anchor found no count for — a
|
||||
feature's guest, a compilation's stranger — get the deferred pass by
|
||||
their OWN artist id; a record's stats are anchored on its artist
|
||||
alone and cannot reach them */
|
||||
$effect(() => { const rs = rows, k = id; if (rs.length) untrack(() => loadTrackListeners(k, rs)); });
|
||||
const extraCounts = $derived(catalog.listeners[id]?.counts ?? {});
|
||||
const listenersOf = (t) => t.listeners ?? extraCounts[t.id] ?? null;
|
||||
const peak = $derived(rows.reduce((m, t) => Math.max(m, listenersOf(t) || 0), 0));
|
||||
const showCount = $derived(peak > 0 && rows.length >= 3);
|
||||
const hasUnreleased = $derived(rows.some((t) => t.unreleased));
|
||||
const fmtCount = (n) => (!n ? "" : n >= 1e6 ? (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M" : n >= 1e3 ? Math.round(n / 1e3) + "k" : String(n));
|
||||
@@ -187,7 +194,7 @@
|
||||
<span class="b">
|
||||
<span class="t">{t.title}{#if hit}<span class="tick" title="In your library"><Icon name="check" size={11} strokeWidth={3} /></span>{/if}</span>
|
||||
{#if !hit && !libAlbum && current(t) && resolver.active}<span class="m">finding the best source…</span>{/if}
|
||||
{#if showCount}<span class="lbar"><i style="transform: scaleX({(peak ? Math.max(0.04, (t.listeners || 0) / peak) : 0).toFixed(3)})"></i><em>{fmtCount(t.listeners)}</em></span>{/if}
|
||||
{#if showCount}<span class="lbar"><i style="transform: scaleX({(peak ? Math.max(0.04, (listenersOf(t) || 0) / peak) : 0).toFixed(3)})"></i><em>{fmtCount(listenersOf(t))}</em></span>{/if}
|
||||
</span>
|
||||
<span class="d">{t.unreleased ? "Unreleased" : mmss(t.duration_secs)}</span>
|
||||
</button>
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
const artMode = $derived(cfg?.replace_embedded_art === false ? "keep" : "replace");
|
||||
const artEdge = $derived(String(cfg?.embed_art_max_edge ?? 1500));
|
||||
let refetching = $state(false);
|
||||
let refreshingArtists = $state(false);
|
||||
async function saveCfg(patch) {
|
||||
try { await api.setSettings(patch); cfg = await api.getSettings(); } catch (e) { player.error = String(e); }
|
||||
}
|
||||
@@ -136,6 +137,14 @@
|
||||
catch (e) { player.error = String(e); }
|
||||
finally { refetching = false; }
|
||||
}
|
||||
/* the desktop Settings row: a portrait for every artist already here */
|
||||
async function refreshPortraits() {
|
||||
if (refreshingArtists) return;
|
||||
refreshingArtists = true;
|
||||
try { const n = await api.refreshArtistArt(); note(`${n ?? 0} portrait${n === 1 ? "" : "s"} refreshed`); }
|
||||
catch (e) { player.error = String(e); }
|
||||
finally { refreshingArtists = false; }
|
||||
}
|
||||
const ROWS = [
|
||||
["ambience", "image", "Ambience", "the artwork in play, blurred, under every screen"],
|
||||
["accent", "sparkle", "Accent from artwork", "the app takes the record's colour"],
|
||||
@@ -309,6 +318,10 @@
|
||||
<span class="lead"><Icon name="refresh" size={20} /></span>
|
||||
<span class="b"><span class="t">{refetching ? "Upgrading covers…" : "Re-fetch all album covers"}</span><span class="m">every record to the canonical high-resolution cover — about one a second</span></span>
|
||||
</button>
|
||||
<button class="row" onclick={refreshPortraits} disabled={refreshingArtists}>
|
||||
<span class="lead"><Icon name="users" size={20} /></span>
|
||||
<span class="b"><span class="t">{refreshingArtists ? "Refreshing portraits…" : "Refresh artist portraits"}</span><span class="m">a photo for every artist already in your library</span></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sheet-sec">This phone</div>
|
||||
|
||||
Reference in new issue
Block a user