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.
This commit is contained in:
Psychotoxical
2026-06-27 01:46:01 +02:00
committed by GitHub
parent c59bf937e4
commit d5ad275851
2 changed files with 18 additions and 17 deletions
+12 -12
View File
@@ -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 =
+6 -5
View File
@@ -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<BackdropSurface, SurfaceBackdropConfig> = {
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']) },
};