fix: artist hero appears instantly; album names skip romaji conversion
Hero now only waits for getArtist (fast local Navidrome) before revealing. getArtistImage (Last.fm lookup) was the 2-4s bottleneck when uncached — it now resolves async and patches in the artist photo after the hero is already visible. Album card, album detail header, and home hero now display the raw album name from Navidrome instead of converting to romaji when showRomaji is on. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
28489f722a
commit
8f7f1e2635
3 files changed
+43
-49
No files matched your search
@@ -8,7 +8,7 @@ window._imgRetry = function(img, fallback) {
|
||||
if (n > 3 || !img.src) { img.parentNode.innerHTML = `<div class="no-art">${fallback}</div>`; return; }
|
||||
setTimeout(() => { const s = img.src; img.src = ''; img.src = s; }, 1500 * n);
|
||||
};
|
||||
import { coverUrl, dispTitle, dispArtistParts, dispAlbum, dispArtistById, dispAlbumName } from '../api.js';
|
||||
import { coverUrl, dispTitle, dispArtistParts, dispArtistById } from '../api.js';
|
||||
// Circular imports — safe (only used inside event handlers after module init)
|
||||
import { playQueue, toggleStar, playPreview, parseArtists } from '../player/player.js';
|
||||
import { showCtxMenu, openDlModal } from './ui.js';
|
||||
@@ -28,7 +28,7 @@ export function makeCard(title, sub, imgUrl, fallback='💿') {
|
||||
|
||||
export function makeAlbumCard(a) {
|
||||
const cv = coverUrl(a.coverArt, 512);
|
||||
const c = makeCard(dispAlbumName(a.name), dispArtistById(a.artistId, a.artist||''), cv, '💿');
|
||||
const c = makeCard(a.name || '', dispArtistById(a.artistId, a.artist||''), cv, '💿');
|
||||
c.querySelector('.card-art').onclick = (e) => { e.stopPropagation(); quickPlayAlbum(a); };
|
||||
c.querySelector('.card-info').onclick = () => navigate('album-detail', a);
|
||||
return c;
|
||||
|
||||
+39
-45
@@ -1,6 +1,6 @@
|
||||
import { S } from '../state.js';
|
||||
import { esc, toast } from '../utils.js';
|
||||
import { api, coverUrl, itunesSearch, itunesLookupAlbum, itunesLookupArtistSongs, itunesLookupArtistAlbums, fetchRomajiIndex, dispArtistById, dispAlbumName, dispAlbum } from '../api.js';
|
||||
import { api, coverUrl, itunesSearch, itunesLookupAlbum, itunesLookupArtistSongs, itunesLookupArtistAlbums, fetchRomajiIndex, dispArtistById } from '../api.js';
|
||||
import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem } from '../ui/components.js';
|
||||
import { playQueue, dlSongLocal, toggleStar, parseArtists } from '../player/player.js';
|
||||
import { showCtxMenu, openDlModal } from '../ui/ui.js';
|
||||
@@ -87,8 +87,6 @@ export async function loadArtistDetail(artist) {
|
||||
const knownItunes = artist.itunesArtistId || null;
|
||||
|
||||
// Fire all requests immediately so they run in parallel.
|
||||
// Hero only needs imgP + albumP (both fast / usually cached) — await those
|
||||
// first so the hero appears instantly without waiting for iTunes or events.
|
||||
const imgP = hasND ? getArtistImage(artist.id) : Promise.resolve(null);
|
||||
const albumP = hasND ? api('getArtist', `&id=${artist.id}`) : Promise.resolve(null);
|
||||
const infoP = hasND ? api('getArtistInfo2',`&id=${artist.id}&count=8`) : Promise.resolve(null);
|
||||
@@ -96,8 +94,42 @@ export async function loadArtistDetail(artist) {
|
||||
const iArtistP = knownItunes ? Promise.resolve([{ artistId: knownItunes }]) : itunesSearch(artist.name, 'musicArtist', 5);
|
||||
const eventsP = fetchEvents(artist.name);
|
||||
|
||||
// Phase 1 — hero (fast: image usually cached from listing, getArtist is local)
|
||||
const [imgRes, albumRes] = await Promise.allSettled([imgP, albumP]);
|
||||
// Phase 1 — hero: only wait for albumP (fast local Navidrome call, no external API).
|
||||
// imgP (getArtistImage) may be slow when uncached — it calls Last.fm via Navidrome.
|
||||
// We reveal the hero with the album cover immediately, then patch in the artist photo async.
|
||||
const [albumRes] = await Promise.allSettled([albumP]);
|
||||
|
||||
const _firstAlbums = albumRes.status === 'fulfilled' ? (albumRes.value?.artist?.album || []) : [];
|
||||
const _bgCover = _firstAlbums[0] ? coverUrl(_firstAlbums[0].coverArt, 512) : null;
|
||||
|
||||
const _revealHero = () => requestAnimationFrame(() => {
|
||||
hero.style.transition = 'opacity 0.25s ease';
|
||||
hero.style.opacity = '1';
|
||||
});
|
||||
|
||||
const bgEl = document.getElementById('artist-hero-bg');
|
||||
const pfpWrap = document.getElementById('artist-hero-pfp-wrap');
|
||||
|
||||
if (_bgCover) {
|
||||
hero.style.background = '';
|
||||
const _pre = new Image();
|
||||
_pre.onload = _pre.onerror = () => { if (bgEl) bgEl.style.backgroundImage = `url('${_bgCover}')`; _revealHero(); };
|
||||
_pre.src = _bgCover;
|
||||
} else {
|
||||
_revealHero();
|
||||
}
|
||||
|
||||
// Patch artist photo in async — resolves immediately if cached, otherwise after Last.fm
|
||||
imgP.then(img => {
|
||||
if (!img || !pfpWrap) return;
|
||||
const pfpImg = document.createElement('img');
|
||||
pfpImg.className = 'artist-hero-pfp';
|
||||
pfpImg.alt = '';
|
||||
pfpImg.onload = pfpImg.onerror = () => { if (bgEl) bgEl.style.backgroundImage = `url('${img}')`; };
|
||||
pfpImg.src = img;
|
||||
pfpWrap.prepend(pfpImg);
|
||||
document.getElementById('artist-hero-pfp-initial')?.style.setProperty('display', 'none');
|
||||
}).catch(() => {});
|
||||
|
||||
// Phase 2 — content (may be slow: Last.fm bio, iTunes, events)
|
||||
const [infoRes, topRes, iArtistRes, eventsRes] = await Promise.allSettled([infoP, topP, iArtistP, eventsP]);
|
||||
@@ -119,44 +151,6 @@ export async function loadArtistDetail(artist) {
|
||||
])
|
||||
: [{ status: 'rejected' }, { status: 'rejected' }];
|
||||
|
||||
// ── Hero ── non-blocking: set bg/pfp immediately, fade in on image load, tint after
|
||||
const img = imgRes.status === 'fulfilled' ? imgRes.value : null;
|
||||
const _firstAlbums = albumRes.status === 'fulfilled' ? (albumRes.value?.artist?.album || []) : [];
|
||||
const _bgCover = _firstAlbums[0] ? coverUrl(_firstAlbums[0].coverArt, 512) : null;
|
||||
const bgSrc = img || _bgCover;
|
||||
|
||||
const _revealHero = () => requestAnimationFrame(() => {
|
||||
hero.style.transition = 'opacity 0.25s ease';
|
||||
hero.style.opacity = '1';
|
||||
});
|
||||
|
||||
if(img) {
|
||||
if(bgSrc) hero.style.background = '';
|
||||
const bgEl = document.getElementById('artist-hero-bg');
|
||||
const pfpWrap = document.getElementById('artist-hero-pfp-wrap');
|
||||
if(pfpWrap) {
|
||||
const pfpImg = document.createElement('img');
|
||||
pfpImg.className = 'artist-hero-pfp';
|
||||
pfpImg.alt = '';
|
||||
pfpImg.onload = pfpImg.onerror = () => {
|
||||
// image is now in cache — set bg background-image here so it renders instantly
|
||||
if(bgEl && bgSrc) bgEl.style.backgroundImage = `url('${bgSrc}')`;
|
||||
_revealHero();
|
||||
};
|
||||
pfpImg.src = img;
|
||||
pfpWrap.prepend(pfpImg);
|
||||
document.getElementById('artist-hero-pfp-initial')?.style.setProperty('display', 'none');
|
||||
}
|
||||
} else if(bgSrc) {
|
||||
hero.style.background = '';
|
||||
const bgEl = document.getElementById('artist-hero-bg');
|
||||
const _pre = new Image();
|
||||
_pre.onload = _pre.onerror = () => { if(bgEl) bgEl.style.backgroundImage = `url('${bgSrc}')`; _revealHero(); };
|
||||
_pre.src = bgSrc;
|
||||
} else {
|
||||
_revealHero();
|
||||
}
|
||||
|
||||
// ── Bio ──
|
||||
const info = infoRes.status === 'fulfilled' ? infoRes.value?.artistInfo2 : null;
|
||||
if(info?.biography && bioEl) {
|
||||
@@ -384,7 +378,7 @@ export async function loadItunesAlbumDetail(album) {
|
||||
export async function loadAlbumDetail(album) {
|
||||
document.getElementById('album-back-btn').onclick = () =>
|
||||
album._fromArtist ? navigate('artist-detail', album._fromArtist) : navigate('albums');
|
||||
document.getElementById('album-detail-name').textContent = dispAlbumName(album.name);
|
||||
document.getElementById('album-detail-name').textContent = album.name || '';
|
||||
const artistEl = document.getElementById('album-detail-artist');
|
||||
artistEl.innerHTML = parseArtists(album)
|
||||
.map(a => `<span class="song-link" onclick="event.stopPropagation();s_goArtist(event,'${esc(a.originalName||a.name)}','${esc(a.id||'')}')">${esc(dispArtistById(a.id, a.name))}</span>`)
|
||||
@@ -400,7 +394,7 @@ export async function loadAlbumDetail(album) {
|
||||
const totalMin = Math.round(songs.reduce((s,t) => s+(t.duration||0), 0) / 60);
|
||||
document.getElementById('album-detail-stats').textContent = `${songs.length} Titel · ${r.album.year||''} · ${totalMin} Min.`;
|
||||
if (songs[0]) {
|
||||
document.getElementById('album-detail-name').textContent = dispAlbum(songs[0]) || dispAlbumName(album.name);
|
||||
document.getElementById('album-detail-name').textContent = songs[0].album || album.name || '';
|
||||
artistEl.innerHTML = parseArtists(songs[0])
|
||||
.map(a => `<span class="song-link" onclick="event.stopPropagation();s_goArtist(event,'${esc(a.originalName||a.name)}','${esc(a.id||'')}')">${esc(dispArtistById(a.id, a.name))}</span>`)
|
||||
.join('<span style="color:var(--text3)"> • </span>') || '';
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { S } from '../state.js';
|
||||
import { esc } from '../utils.js';
|
||||
import { api, apiCached, apiIsCached, coverUrl, fetchRomajiIndex, dispArtistById, dispAlbumName } from '../api.js';
|
||||
import { api, apiCached, apiIsCached, coverUrl, fetchRomajiIndex, dispArtistById } from '../api.js';
|
||||
import { makeCard, makeAlbumCard, makeSongItem } from '../ui/components.js';
|
||||
import { playQueue } from '../player/player.js';
|
||||
// Circular — safe (called inside event handlers)
|
||||
@@ -60,7 +60,7 @@ async function loadHeroCard() {
|
||||
${thumbUrl ? `<img class="home-hero-cover" src="${thumbUrl}" alt="${esc(album.name)}">` : ''}
|
||||
<div class="home-hero-info">
|
||||
<div class="home-hero-label">${heroLabel}</div>
|
||||
<div class="home-hero-title">${esc(dispAlbumName(album.name))}</div>
|
||||
<div class="home-hero-title">${esc(album.name || '')}</div>
|
||||
<div class="home-hero-artist">${esc(dispArtistById(album.artistId, album.artist || ''))}</div>
|
||||
</div>
|
||||
<button class="home-hero-play" title="Abspielen">
|
||||
|
||||
Reference in new issue
Block a user