From d5ad27585145f6b57c0e1658083a3cf48042af09 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Sat, 27 Jun 2026 01:46:01 +0200 Subject: [PATCH] fix(hero): mainstage backdrop falls back to the album cover, reorder defaults (#1196) * fix(hero): use the album cover as the mainstage backdrop's last fallback The mainstage hero inherited the artist-detail fallback chain, ending on the Navidrome artist image. The hero frames an album, so its last-resort backdrop should be the album's own cover (the layer shown when the feature is off), not the artist image. Resolve it scope-true from the album cover ref already in hand. Artist-detail keeps the artist cover. * fix(hero): default backdrop order to fanart, Navidrome, then banner Lead both heroes with the artist fanart and fall back to the wide banner last, instead of trying the banner first. The fullscreen player keeps fanart then Navidrome (it has no banner). Affects defaults / reset only; saved per-surface orders are untouched and remain user-reorderable. --- src/components/Hero.tsx | 24 ++++++++++++------------ src/store/themeStore.ts | 11 ++++++----- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 92119000..6d72a7ff 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -8,8 +8,7 @@ import { Play, ListPlus, ChevronLeft, ChevronRight } from 'lucide-react'; import { CoverArtImage } from '../cover/CoverArtImage'; import { useAlbumCoverRef } from '../cover/useLibraryCoverRef'; import { useArtistBanner, useArtistFanart } from '../cover/useArtistFanart'; -import { usePlaybackCoverArt } from '../cover/usePlaybackCoverArt'; -import { artistCoverRef } from '../cover/ref'; +import { useCoverArt } from '../cover/useCoverArt'; import { useHeroBackdrop } from '../cover/useHeroBackdrop'; import { useCachedUrl } from './CachedImage'; import { usePlayerStore } from '../store/playerStore'; @@ -343,11 +342,14 @@ export default function Hero({ albums: albumsProp }: HeroProps = {}) { const heroCoverRef = useAlbumCoverRef(album?.id, album?.coverArt); const albumId = album?.id; - // Mainstage hero backdrop — the album artist's fanart, resolved exactly like - // the artist-detail header (banner → 16:9 fanart → Navidrome artist cover). + // Mainstage hero backdrop — the album artist's fanart (banner → 16:9 fanart), + // but its LAST fallback is the album's own Navidrome cover, not the artist + // image: the hero frames an album, so its base layer stays the album cover + // (the same backdrop shown when the feature is off). The artist-detail header + // keeps the artist cover as its last fallback — that surface frames an artist. // Fed entirely from the album already in hand (artist id + name + album title), // so there is no getArtist/getAlbum round-trip: the MBID lookup + fanart fetch - // live Rust-side in cover_cache, and the artist cover ref needs only the id. + // live Rust-side in cover_cache. const heroArtist = useMemo( () => (album ? deriveAlbumArtistRefs(album)[0] : undefined), [album], @@ -361,15 +363,13 @@ export default function Hero({ albums: albumsProp }: HeroProps = {}) { artistName: heroArtist?.name, albumTitle: album?.name, }); - const heroArtistCoverRef = useMemo( - () => (heroArtistId ? artistCoverRef(heroArtistId) : undefined), - [heroArtistId], - ); - const ndArtist = usePlaybackCoverArt(heroArtistCoverRef, HERO_BG_CSS_PX, { fullRes: true }); - const ndArtistUrl = useCachedUrl(ndArtist.src, ndArtist.cacheKey, true); + // Last-fallback layer: the album's own Navidrome cover (HERO_BG_CSS_PX, full + // res), resolved scope-true from the album cover ref already in hand. + const ndAlbum = useCoverArt(heroCoverRef, HERO_BG_CSS_PX, { surface: 'sparse', fullRes: true }); + const ndAlbumUrl = useCachedUrl(ndAlbum.src, ndAlbum.cacheKey, true); const heroBackdrop = useHeroBackdrop( mainstageBackdrop.sources, - { banner: heroBanner, fanart: heroFanart, navidrome: ndArtistUrl }, + { banner: heroBanner, fanart: heroFanart, navidrome: ndAlbumUrl }, albumId, ); const showHeroBackdrop = diff --git a/src/store/themeStore.ts b/src/store/themeStore.ts index c2754196..a00da4cb 100644 --- a/src/store/themeStore.ts +++ b/src/store/themeStore.ts @@ -15,13 +15,14 @@ const allEnabled = (sources: BackdropSource[]): BackdropSourcePref[] => sources.map((source) => ({ source, enabled: true })); /** - * Defaults preserve today's behaviour: both heroes resolve banner → fanart → - * Navidrome; the fullscreen player has no banner (a wide strip suits neither a - * portrait nor a square stage), so it offers fanart → Navidrome only. + * Defaults: both heroes resolve fanart → Navidrome → banner — the artist image + * leads, with the wide banner as a last resort. The fullscreen player has no + * banner (a wide strip suits neither a portrait nor a square stage), so it + * offers fanart → Navidrome only. All user-reorderable per surface. */ const DEFAULT_BACKDROPS: Record = { - mainstageHero: { enabled: true, sources: allEnabled(['banner', 'fanart', 'navidrome']) }, - artistDetailHero: { enabled: true, sources: allEnabled(['banner', 'fanart', 'navidrome']) }, + mainstageHero: { enabled: true, sources: allEnabled(['fanart', 'navidrome', 'banner']) }, + artistDetailHero: { enabled: true, sources: allEnabled(['fanart', 'navidrome', 'banner']) }, fullscreenPlayer: { enabled: true, sources: allEnabled(['fanart', 'navidrome']) }, };