diff --git a/src/ui/components.js b/src/ui/components.js index 5acdee3..ed5f4bf 100644 --- a/src/ui/components.js +++ b/src/ui/components.js @@ -8,7 +8,7 @@ window._imgRetry = function(img, fallback) { if (n > 3 || !img.src) { img.parentNode.innerHTML = `
${fallback}
`; 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; diff --git a/src/views/detail.js b/src/views/detail.js index 809fe05..9833eb1 100644 --- a/src/views/detail.js +++ b/src/views/detail.js @@ -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 => `${esc(dispArtistById(a.id, a.name))}`) @@ -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 => `${esc(dispArtistById(a.id, a.name))}`) .join(' โ€ข ') || ''; diff --git a/src/views/home.js b/src/views/home.js index a539e4b..df5a80a 100644 --- a/src/views/home.js +++ b/src/views/home.js @@ -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 ? `${esc(album.name)}` : ''}
${heroLabel}
-
${esc(dispAlbumName(album.name))}
+
${esc(album.name || '')}
${esc(dispArtistById(album.artistId, album.artist || ''))}