mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
feat(hero): album-artist backdrop, configurable per-surface sources, and prefetch (#1193)
* refactor(cover): extract shared pickArtistBackdrop priority helper
* feat(hero): show the album artist's fanart as the mainstage hero backdrop
* feat(settings): configurable per-surface artist backdrop sources
Each artist-backdrop surface (mainstage hero, artist-detail header, fullscreen player) gets its own enable toggle + an ordered, individually-toggleable source list, configured under External Artwork Scraper on the Integrations tab (shown when the scraper is on). Reorder via the shared useDragSource/psy-drop drag infra plus keyboard-accessible up/down buttons; each source has its own on/off.
The shared chooser pickArtistBackdrop is generalised to resolveBackdrop + backdropFromConfig (ordered candidate list with the same pending/miss/centred-framing semantics), so all three surfaces resolve identically. themeStore persist bumped to v2; defaults reproduce today's order, so there is no visible change without user action.
Gating decoupled: the three surfaces are gated solely by their own per-surface flag, not by enableCoverArtBackground (which stays scoped to album/playlist-header cover blur). Reorder maths extracted to a pure, unit-tested module. i18n en + de (other locales TODO before PR).
Tests: resolver (10) + reorder (7).
* feat(cover): make the ensure queue surface-aware for artist backdrops
coverEnsureQueued now threads optional CoverEnsureOpts through to the Rust ensure and weaves the external surface into the in-flight key, so the fanart and banner surfaces of one artist no longer collide on one download chain. External surfaces also bypass the disk-src memory short-circuit (their {tier}-{surface}.webp never seeds those caches, and the canonical cover must not read as a hit). New thin ensureArtistBackdropQueued wrapper. Backward-compatible: plain covers append nothing to the key; the 5 queue tests stay green.
* fix(cover): reset the artist external-image hook synchronously on artist change
The hook reset src in an effect (one render late), so for the render between an artist change and that effect a consumer read the *previous* artist's resolved image. The mainstage hero then froze (and cached into per-album memory) a neighbouring slide's banner. Reset synchronously via the React adjust-state-on-prop-change pattern. Also removes brief stale-image flashes on the artist-detail header and fullscreen player.
* feat(hero): prefetch artist backdrops and show-ready-now / upgrade-on-re-entry
warmHomeMainstageCovers now prefetches each hero slide's artist backdrop (banner/fanart) at static slide-index priorities (idx1=high, idx0=low, rest=middle; no reprioritise on navigation), then predecodes every slide already on disk. useHeroBackdrop shows the best source ready at entry (Navidrome on a cold first visit, the prefetched/cached external one on re-entry) and freezes that source choice for the visit so nothing swaps mid-dwell; the url is derived live from the frozen choice, with a per-album disk memory for re-entry. HeroBg now crossfades only after the image bytes load (onLoad/complete gate + onError + fallback). Inert when the scraper is off. Tests: per-album memory (5).
* docs(changelog): configurable artist backdrops + mainstage hero (PR #1193)
CHANGELOG Added entry, settingsCredits line, and the What's New Artist-artwork highlight extended to cover the mainstage hero backdrop and per-place source config.
* docs(changelog): fold mainstage hero + per-place backdrops into the fanart entry (PR #1193)
Merge the configurable-backdrops changes into the existing 'Artist artwork from fanart.tv' block (now PR #1137 and #1193) instead of a separate entry.
* fix(hero): revert HeroBg load-gate that blanked the app (Maximum update depth)
The byte-load gate I added drove the crossfade reveal from an inline img ref + onLoad that re-fires on every render and scheduled a setTimeout each call; frequent re-renders (playback, marquee) stacked nested updates until React threw 'Maximum update depth exceeded' — and with no ErrorBoundary the whole window blanked. Reverted HeroBg to the proven timer-based crossfade. The hero only ever receives ready/predecoded urls, so the gate was cosmetic.
* fix(hero): gate the HeroBg crossfade on image load to stop the slide flicker
The bare 20 ms reveal faded a layer in before its bytes were ready (notably the Navidrome raw url), so a slide change flickered. Now an Image() preloader in the [url] effect reveals the layer on load (or a cached complete check), with a fallback. Everything is scheduled once per url — no per-render <img> ref/onLoad — so unlike the reverted gate it can't stack nested updates.
* feat(i18n): translate the backdrop-source settings into the remaining 10 locales
Adds the per-surface backdrop config strings (es, fr, nl, zh, nb, ru, ro, ja, hu, pl) and extends externalArtworkDesc to mention the mainstage hero where the key exists (ja has none → falls back to en).
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
/* Backdrop source priority lists (Appearance → Cover Art Background).
|
||||
Each row reorders via the shared psy-drop drag system (grip) or the ↑/↓
|
||||
buttons; the per-row switch drops a source out of the resolution chain
|
||||
without losing its place. */
|
||||
/* One surface (Mainstage / Artist page / Fullscreen): its enable toggle and,
|
||||
when on, its source list. */
|
||||
.backdrop-surface-block {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.backdrop-surface-block:first-of-type {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.backdrop-source-list {
|
||||
list-style: none;
|
||||
margin: 6px 0 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.backdrop-source-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-card);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.backdrop-source-row--off .backdrop-source-name {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Drop target: an accent line across the top edge, mirroring the sidebar reorder. */
|
||||
.backdrop-source-row--drop {
|
||||
border-color: color-mix(in srgb, var(--accent) 55%, transparent);
|
||||
}
|
||||
.backdrop-source-row--drop::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
top: -3px;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.backdrop-source-grip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--text-muted);
|
||||
cursor: grab;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.backdrop-source-grip:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.backdrop-source-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.backdrop-source-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.backdrop-source-move {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-secondary);
|
||||
transition: background var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
.backdrop-source-move:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.backdrop-source-move:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
@@ -95,3 +95,4 @@
|
||||
@import './orbit-session-top-strip.css';
|
||||
@import './back-to-top.css';
|
||||
@import './error-boundary.css';
|
||||
@import './backdrop-source-list.css';
|
||||
|
||||
Reference in New Issue
Block a user